前面一篇文章setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop详细讲解了浏览器和Node.js的异步API及其底层原理Event Loop。本文会讲一下不用原生API怎么达到异步的效果,也就是发布订阅模式。发布订阅模式在面试中也是高频考点,本文会自己实现一个发布订阅模式,弄懂了他的原理后,我们就可以去读Node.js的EventEmitter源码,这也是一个典型的发布订阅模式。
本文所有例子已经上传到GitHub,同一个repo下面还有我所有博文和例子:
https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/DesignPatterns/PubSub
为什么要用发布订阅模式在没有Promise之前,我们使用异步API的时候经常会使用回调,但是如果有几个互相依赖的异步API调用,回调层级太多可能就会陷入“回调地狱”。下面代码演示了假如我们有三个网络请求,第二个必须等第一个结束才能发出,第三个必须等第二个结束才能发起,如果我们使用回调就会变成这样:
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运行的。这个例子里面有三层回调,我们已经有点晕了,如果再多几层,那真的就是“地狱”了。
发布订阅模式发布订阅模式是一种设计模式,并不仅仅用于JS中,这种模式可以帮助我们解开“回调地狱”。他的流程如下图所示:
消息中心:负责存储消息与订阅者的对应关系,有消息触发时,负责通知订阅者
订阅者:去消息中心订阅自己感兴趣的消息
发布者:满足条件时,通过消息中心发布消息
有了这种模式,前面处理几个相互依赖的异步API就不用陷入"回调地狱"了,只需要让后面的订阅前面的成功消息,前面的成功后发布消息就行了。
自己实现一个发布订阅模式知道了原理,我们自己来实现一个发布订阅模式,这次我们使用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的EventEmitterNode.js的EventEmitter思想跟我们前面的例子是一样的,不过他有更多的错误处理和更多的API,源码在GitHub上都有:https://github.com/nodejs/node/blob/master/lib/events.js。我们挑几个API看一下:
构造函数代码传送门: