当前位置: 首页 > 后端技术 > Node.js

从发布-订阅模型入手阅读Node.jsEventEmitter源码

时间:2023-04-03 15:54:02 Node.js

上一篇中setTimeout和setImmediate谁先执行?原理事件循环。这篇文章会讲到如何在不使用原生API的情况下实现异步效果,即发布-订阅模型。发布-订阅模式也是面试中的高频考点。本文将自己实现一个发布-订阅模型。了解了它的原理之后,我们可以去阅读Node.jsEventEmitter的源码,这也是一个典型的发布-订阅模型。本文中的所有示例都已上传到GitHub,我所有的博文和示例都在同一个repo下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/DesignPatterns/为什么要使用PubSub发布-订阅模式在Promise之前,我们在使用异步API的时候经常会用到回调,但是如果有几个相互依赖的异步API调用,太多的回调层级可能会陷入“回调地狱”。下面的代码演示了如果我们有三个网络请求,第二个必须等待第一个结束才能发起,第三个必须等待第二个结束才能发起。如果我们使用回调,就会变成这样:constrequest=require("request");request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('gettimes1');request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('gettimes2');request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('得到时间3');}})}})}});由于浏览器端ajax会存在跨域问题,所以我用Node.js运行上面的例子。这个例子里有三层回调,我们已经有点晕了。如果层数多了,那就真的是“地狱”了。发布订阅模式发布订阅模式是一种设计模式,不仅仅用在JS中,这种模式可以帮助我们解开“回调地狱”。他的流程如下图所示:消息中心:负责存储消息与订阅者的对应关系,并在消息触发时通知订阅者订阅者:去消息中心订阅你感兴趣的消息消息中心发布消息。使用这种模式,您在处理多个相互依赖的异步API时就不必陷入“回调地狱”。只需要让后者订阅上一个成功消息,上一个成功后发布消息即可。自己实现一个发布订阅模型知道了原理,我们来自己实现一个发布订阅模型。这次我们使用ES6类来实现它。如果你对JS面向对象或者ES6类不熟悉,请看这篇文章:classPubSub{constructor(){//一个对象存储所有的消息订阅//每条消息对应一个数组,数组结构如下//{//"event1":[cb1,cb2]//}this.events={}}subscribe(event,callback){if(this.events[event]){//如果有人订阅了,key已经存在,只需添加它this.events[event].push(callback);}else{//如果没有人订阅,则创建一个数组并将回调放入其中。this.events[event]=[callback]}}publish(event,...args){//执行所有订阅者的回调constsubscribedEvents=this.events[event];if(subscribedEvents&&subscribedEvents.length){subscribedEvents.forEach(callback=>{callback.call(this,...args);});}}unsubscribe(event,callback){//删除订阅,保留其他订阅constsubscribedEvents=this.events[event];if(subscribedEvents&&subscribedEvents.length){this.events[event]=this.events[event].filter(cb=>cb!==callback)}}}和我们一起解决回调地狱我们自己的PubSub,我们可以用它来解决之前的回调地狱问题:constrequest=require("request");constpubSub=newPubSub();request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('gettimes1');//发布请求1成功消息pubSub.publish('request1Success');}});//订阅请求1的成功消息,然后发起请求2pubSub.subscribe('request1Success',()=>{request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('gettimes2');//发布请求2成功消息pubSub.publish('request2Success');}});})//订阅请求2成功消息,然后发起请求3pubSub.subscribe('request2Success',()=>{request('https://www.baidu.com',function(error,response){if(!error&&response.statusCode==200){console.log('gettimes3');//发布请求3成功消息pubSub.publish('request3Success');}});})Node.js的EventEmitterNode.js的EventEmitter的思想和我们前面的例子是一样的,只是多了一些错误处理和更多的API,源码在GitHub上有:https://github.com/nodejs/node/blob/master/lib/events.js我们挑几个API来看看:构造函数代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L64构造函数很简单,就一行代码,主要逻辑在EventEmitter.init中:EventEmitter.init也做一些初始化工作,this._events和这个写的一样由我们自己。events函数也是一样的,用来存放订阅的事件。我在图上用箭头标记了核心代码。这里要注意一点,如果一类事件只有一个订阅,this._events直接就是那个函数,而不是数组。我们在源码中会多次看到这个判断,这是为了提高性能而写的。订阅事件代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L405EventEmitter订阅事件的接口是on和addListener。从源码中我们可以看出这两个方法是一模一样的:都调用了_addListener,对参数进行了错误的判断和处理,核心代码依然是给this添加事件。_events:发布事件代码传送门:https://github。com/nodejs/node/blob/master/lib/events.js#L263EventEmitter发布事件的API是emit。该API会对“error”类型事件进行特殊处理,即抛出错误:如果不是错误类型事件,取出订阅的回调事件执行:取消订阅代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L450EventEmitter取消订阅的接口是removeListener和off,两者完全一样。EventEmitter的unsubscribeAPI不仅会删除对应的订阅,删除后还会发出removeListener事件通知外界。这里也会判断this._events中对应的type。如果只有一个,也就是说这个类型的类型是function,那么key就直接删除了。如果有多个订阅,将找到并删除该订阅。他。如果删除所有订阅,直接清空this._events即可:总结本文讲解了发布订阅模型的原理,自己实现了一个简单的发布订阅模型。了解了原理后,又阅读了Node.js的EventEmitter模块的源码,进一步学习了生产环境下发布-订阅模式的写法。总结一下,发布-订阅模式有以下特点:解决了“回调地狱”,解耦了多个模块。如果不知道对方的存在,你关心的事件可能会在很远的角落发布,你无法通过代码跳转直接找到事件发布的地方。调试起来可能有点困难。文末,感谢您抽出宝贵时间阅读本文。如果这篇文章给了你一点帮助或者启发,请不要吝啬你的点赞和GitHubstar。您的支持是作者继续创作的动力。欢迎关注我的公众号进取大前端第一时间获取优质原创~《前端进阶知识》系列文章源码地址:https://github.com/dennis-jiang/前端知识