后面一篇文章 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 的 EventEmitter
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
置空:
总结
本文解说了公布订阅模式的原理,并本人实现了一个简略的公布订阅模式。在理解了原理后,还去读了 Node.js 的 EventEmitter
模块的源码,进一步学习了生产环境的公布订阅模式的写法。总结下来公布订阅模式有以下特点:
- 解决了“回调天堂”
- 将多个模块进行理解耦,本人执行时,不须要晓得另一个模块的存在,只须要关怀公布进去的事件就行
- 因为多个模块能够不晓得对方的存在,本人关怀的事件可能是一个很边远的旮旯公布进去的,也不能通过代码跳转间接找到公布事件的中央,debug 的时候可能会有点艰难。
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges