前面一篇文章setTimeout和setImmediate到底谁先执行,本文让你完全理解Event Loop详细讲解了浏览器和Node.js的异步API及其底层原理Event Loop。本文会讲一下不用原生API怎么达到异步的效果,也就是发布订阅模式。发布订阅模式在面试中也是高频考点,本文会本身实现一个发布订阅模式,弄懂了他的原理后,咱们就能够去读Node.js的EventEmitter
源码,这也是一个典型的发布订阅模式。javascript
本文全部例子已经上传到GitHub,同一个repo下面还有我全部博文和例子:java
https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/DesignPatterns/PubSubnode
在没有Promise
以前,咱们使用异步API的时候常常会使用回调,可是若是有几个互相依赖的异步API调用,回调层级太多可能就会陷入“回调地狱”。下面代码演示了假如咱们有三个网络请求,第二个必须等第一个结束才能发出,第三个必须等第二个结束才能发起,若是咱们使用回调就会变成这样:git
const request = require("request"); request('https://www.baidu.com', function (error, response) { if (!error && response.statusCode == 200) { console.log('get times 1'); request('https://www.baidu.com', function(error, response) { if (!error && response.statusCode == 200) { console.log('get times 2'); request('https://www.baidu.com', function(error, response) { if (!error && response.statusCode == 200) { console.log('get times 3'); } }) } }) } });
因为浏览器端ajax会有跨域问题,上述例子我是用Node.js运行的。这个例子里面有三层回调,咱们已经有点晕了,若是再多几层,那真的就是“地狱”了。github
发布订阅模式是一种设计模式,并不只仅用于JS中,这种模式能够帮助咱们解开“回调地狱”。他的流程以下图所示:面试
- 消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者
- 订阅者:去消息中心订阅本身感兴趣的消息
- 发布者:知足条件时,经过消息中心发布消息
有了这种模式,前面处理几个相互依赖的异步API就不用陷入"回调地狱"了,只须要让后面的订阅前面的成功消息,前面的成功后发布消息就好了。ajax
知道了原理,咱们本身来实现一个发布订阅模式,此次咱们使用ES6的class来实现,若是你对JS的面向对象或者ES6的class还不熟悉,请看这篇文章:设计模式
class PubSub { constructor() { // 一个对象存放全部的消息订阅 // 每一个消息对应一个数组,数组结构以下 // { // "event1": [cb1, cb2] // } this.events = {} } subscribe(event, callback) { if(this.events[event]) { // 若是有人订阅过了,这个键已经存在,就往里面加就行了 this.events[event].push(callback); } else { // 没人订阅过,就建一个数组,回调放进去 this.events[event] = [callback] } } publish(event, ...args) { // 取出全部订阅者的回调执行 const subscribedEvents = this.events[event]; if(subscribedEvents && subscribedEvents.length) { subscribedEvents.forEach(callback => { callback.call(this, ...args); }); } } unsubscribe(event, callback) { // 删除某个订阅,保留其余订阅 const subscribedEvents = this.events[event]; if(subscribedEvents && subscribedEvents.length) { this.events[event] = this.events[event].filter(cb => cb !== callback) } } }
有了咱们本身的PubSub
,咱们就能够用它来解决前面的毁掉地狱问题了:跨域
const request = require("request"); const pubSub = new PubSub(); request('https://www.baidu.com', function (error, response) { if (!error && response.statusCode == 200) { console.log('get times 1'); // 发布请求1成功消息 pubSub.publish('request1Success'); } }); // 订阅请求1成功的消息,而后发起请求2 pubSub.subscribe('request1Success', () => { request('https://www.baidu.com', function (error, response) { if (!error && response.statusCode == 200) { console.log('get times 2'); // 发布请求2成功消息 pubSub.publish('request2Success'); } }); }) // 订阅请求2成功的消息,而后发起请求3 pubSub.subscribe('request2Success', () => { request('https://www.baidu.com', function (error, response) { if (!error && response.statusCode == 200) { console.log('get times 3'); // 发布请求3成功消息 pubSub.publish('request3Success'); } }); })
Node.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
跟咱们本身写的this.events
功能是同样的,用来存储订阅的事件。核心代码我在图上用箭头标出来了。这里须要注意一点,若是一个类型的事件只有一个订阅,this._events
就直接是那个函数了,而不是一个数组,在源码里面咱们会屡次看到对这个进行判断,这样写是为了提升性能。
代码传送门: https://github.com/nodejs/node/blob/master/lib/events.js#L405
EventEmitter
订阅事件的API是on
和addListener
,从源码中咱们能够看出这两个方法是彻底同样的:
这两个方法都是调用了_addListener
,这个方法对参数进行了判断和错误处理,核心代码仍然是往this._events
里面添加事件:
代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L263
EventEmitter
发布事件的API是emit
,这个API里面会对"error"类型的事件进行特殊处理,也就是抛出错误:
若是不是错误类型的事件,就把订阅的回调事件拿出来执行:
代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L450
EventEmitter
里面取消订阅的API是removeListener
和off
,这两个是彻底同样的。EventEmitter
的取消订阅API不只仅会删除对应的订阅,在删除后还会emit一个removeListener
事件来通知外界。这里也会对this._events
里面对应的type
进行判断,若是只有一个,也就是说这个type
的类型是function
,会直接删除这个键,若是有多个订阅,就会找出这个订阅,而后删掉他。若是全部订阅都删完了,就直接将this._events
置空:
这里再提一个很类似的设计模式:观察者模式,有些文章认为他和发布订阅模式是同样的,有些认为他们是有区别的。笔者认为他更像一个低配版的发布订阅模式,咱们来实现一个看看:
class Subject { constructor() { // 一个数组存放全部的订阅者 // 每一个消息对应一个数组,数组结构以下 // [ // { // observer: obj, // action: () => {} // } // ] this.observers = []; } addObserver(observer, action) { // 将观察者和回调放入数组 this.observers.push({observer, action}); } notify(...args) { // 执行每一个观察者的回调 this.observers.forEach(item => { const {observer, action} = item; action.call(observer, ...args); }) } } const subject = new Subject(); // 添加一个观察者 subject.addObserver({name: 'John'}, function(msg){ console.log(this.name, 'got message: ', msg); }) // 再添加一个观察者 subject.addObserver({name: 'Joe'}, function(msg) { console.log(this.name, 'got message: ', msg); }) // 通知全部观察者 subject.notify('tomorrow is Sunday');
上述代码的输出是:
经过这个输出能够看出一旦调了通知的方法notify
,全部观察者都会收到通知,并且会收到一样的信息。而发布订阅模式还能够自定义须要接受的通知,因此说观察者模式是低配版的发布订阅模式。
本文讲解了发布订阅模式的原理,并本身实现了一个简单的发布订阅模式。在了解了原理后,还去读了Node.js的EventEmitter
模块的源码,进一步学习了生产环境的发布订阅模式的写法。总结下来发布订阅模式有如下特色:
文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges