为何写这篇文章?javascript
说一下 Node.js 哪里应用到了发布/订阅模式Events 模块在实际项目开发中有使用过吗?具体应用场景是?前端
Events 监听函数的执行顺序是异步仍是同步的?java
说几个 Events 模块的经常使用函数吧?node
模拟实现 Node.js 的核心模块 Eventsgit
文章首发Github 博客开源项目 https://github.com/koala-codi...程序员
发布/订阅者模式
应该是我在开发过程当中遇到的最多的设计模式。发布/订阅者模式
,也能够称之为消息机制,定义了一种依赖关系,这种依赖关系能够理解为 1对N
(注意:不必定是1对多,有时候也会1对1哦),观察者们同时监听某一个对象相应的状态变换,一旦变化则通知到全部观察者,从而触发观察者相应的事件,该设计模式解决了主体对象与观察者之间功能的耦合
。github
在现实生活中,警察抓小偷是一个典型的观察者模式「这以一个惯犯在街道逛街而后被抓为例子」,这里小偷就是被观察者,各个干警就是观察者,干警时时观察着小偷,当小偷正在偷东西「就给干警发送出一条信号,实际上小偷不可能告诉干警我有偷东西」,干警收到信号,出击抓小偷。这就是一个观察者模式面试
生活中就像是去报社订报纸,你喜欢读什么报就去报社去交钱订阅,当发布了新报纸的时候,报社会向全部订阅了报纸的每个人发送一份,订阅者就能够接收到。编程
我这个微信公号做者是发布者,您这些微信用户是订阅者「我发送一篇文章的时候,关注了【程序员成长指北】的订阅者们均可以收到文章。bootstrap
以你们订阅公众号
为例子,看看发布/订阅模式
如何实现的。(以订阅报纸做为例子的缘由,能够增长一个type
参数,用于区分订阅不一样类型的公众号,若有的人订阅的是前端公众号,有的人订阅的是 Node.js 公众号,使用此属性来标记。这样和接下来要讲的 EventEmitter 源码更相符,另外一个缘由是这样你只要打开一个订阅号文章是否是就想到了发布-订阅者模式呢。)
代码以下:
let officeAccounts ={ // 初始化定义一个存储类型对象 subscribes:{ 'any':[] }, // 添加订阅号 subscribe:function(type='any',fn){ if(!this.subscribes[type]){ this.subscribes[type] = []; } this.subscribes[type].push(fn);//将订阅方法存在数组中 }, // 退订 unSubscribe:function(type='any',fn){ this.subscribes[type] = this.subscribes[type].filter((item)=>{ return item!=fn;// 将退订的方法从数组中移除 }); }, // 发布订阅 publish:function(type='any',...args){ this.subscribes[type].forEach(item => { item(...args);// 根据不一样的类型调用相应的方法 }); } }
以上就是一个最简单的观察者模式的实现,能够看到代码很是的简单,核心原理就是将订阅的方法按分类存在一个数组中,当发布时取出执行便可
接下里看小明订阅【程序员成长指北】文章的代码:
let xiaoming = { readArticle:function (info) { console.log('小明收到的',info); } }; let xiaogang = { readArticle:function (info) { console.log('小刚收到的',info); } }; officeAccounts.subscribe('程序员成长指北',xiaoming.readArticle); officeAccounts.subscribe('程序员成长指北',xiaogang.readArticle); officeAccounts.subscribe('某公众号',xiaoming.readArticle); officeAccounts.unSubscribe('某公众号',xiaoming.readArticle); officeAccounts.publish('程序员成长指北','程序员成长指北的Node文章'); officeAccounts.publish('某公众号','某公众号的文章');
运行结果:
小明收到的 程序员成长指北的Node文章 小刚收到的 程序员成长指北的Node文章
经过观察现实生活中的三个例子以及代码实例发现发布/订阅模式的确是1对N的关系。当发布者的状态发生改变时,全部订阅者都会获得通知。
三要素:
主体和观察者之间彻底透明,全部的消息传递过程都经过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内,而主体和观察者之间实现了彻底的松耦合。对象直接的解耦,异步编程中,能够更松耦合的代码编写。
程序易读性显著下降;多个发布者和订阅者嵌套在一块儿的时候,程序难以跟踪,其实仍是代码不易读,嘿嘿。
Node.js 中的 EventEmitter
模块就是用了发布/订阅这种设计模式,发布/订阅 模式在主体与观察者之间引入消息调度中心,主体和观察者之间彻底透明,所 有的消息传递过程都经过消息调度中心完成,也就是说具体的业务逻辑代码将会是在消息调度中心内完成。
经过Api的对比,来看看Events模块
Events是 Node.js 中一个使用率很高的模块,其它原生node.js模块都是基于它来完成的,好比流、HTTP等。它的核心思想就是 Events 模块的功能就是一个事件绑定与触发
,全部继承自它的实例都具有事件处理的能力。
本模块的官方 Api 讲解不是直接带你们学习文档,而是
经过对比
发布/订阅设计模式本身手写一个版本 Events 的核心代码来学习并记住Api
Events 模块只有一个 EventEmitter 类,首先定义类的基本结构
function EventEmitter() { //私有属性,保存订阅方法 this._events = {}; } //默认设置最大监听数 module.exports = EventEmitter;
on 方法,该方法用于订阅事件(这里 on 和 addListener 说明下),Node.js 源码中这样把它们俩赋值了下,我也不太懂为何?知道的小伙伴能够告诉我为何要这样作哦。
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener;
接下来是咱们对on方法的具体实践:
EventEmitter.prototype.on = EventEmitter.prototype.addListener = function (type, listener, flag) { //保证存在实例属性 if (!this._events) this._events = Object.create(null); if (this._events[type]) { if (flag) {//从头部插入 this._events[type].unshift(listener); } else { this._events[type].push(listener); } } else { this._events[type] = [listener]; } //绑定事件,触发newListener if (type !== 'newListener') { this.emit('newListener', type); } };
由于有其它子类须要继承自EventEmitter,所以要判断子类是否存在_event属性,这样作是为了保证子类必须存在此实例属性。而flag标记是一个订阅方法的插入标识,若是为'true'就视为插入在数组的头部。能够看到,这就是观察者模式的订阅方法实现。
EventEmitter.prototype.emit = function (type, ...args) { if (this._events[type]) { this._events[type].forEach(fn => fn.call(this, ...args)); } };
emit方法就是将订阅方法取出执行,使用call方法来修正this的指向,使其指向子类的实例。
EventEmitter.prototype.once = function (type, listener) { let _this = this; //中间函数,在调用完以后当即删除订阅 function only() { listener(); _this.removeListener(type, only); } //origin保存原回调的引用,用于remove时的判断 only.origin = listener; this.on(type, only); };
once方法很是有趣,它的功能是将事件订阅“一次”,当这个事件触发过就不会再次触发了。其原理是将订阅的方法再包裹一层函数,在执行后将此函数移除便可。
EventEmitter.prototype.off = EventEmitter.prototype.removeListener = function (type, listener) { if (this._events[type]) { //过滤掉退订的方法,从数组中移除 this._events[type] = this._events[type].filter(fn => { return fn !== listener && fn.origin !== listener }); } };
off方法即为退订,原理同观察者模式同样,将订阅方法从数组中移除便可。
EventEmitter.prototype.prependListener = function (type, listener) { this.on(type, listener, true); };
码此方法没必要多说了,调用on方法将标记传为true(插入订阅方法在头部)便可。
以上,就将EventEmitter类的核心方法实现了。
emitter.listenerCount(eventName)
能够获取事件注册的listener
个数emitter.listeners(eventName)
能够获取事件注册的listener
数组副本。//event.js 文件 var events = require('events'); var emitter = new events.EventEmitter(); emitter.on('someEvent', function(arg1, arg2) { console.log('listener1', arg1, arg2); }); emitter.on('someEvent', function(arg1, arg2) { console.log('listener2', arg1, arg2); }); emitter.emit('someEvent', 'arg1 参数', 'arg2 参数');
执行以上代码,运行的结果以下:
$ node event.js listener1 arg1 参数 arg2 参数 listener2 arg1 参数 arg2 参数
手写Events模块代码的时候注意如下几点:
注意:我上面的手写代码并非性能最好和最完善的,目的只是带你们先弄懂记住他。举个例子:
最初的定义EventEmitter类,源码中并非直接定义 this._events = {}
,请看:
function EventEmitter() { EventEmitter.init.call(this); } EventEmitter.init = function() { if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { this._events = Object.create(null); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; };
一样是实现一个类,可是源码中更注意性能,咱们可能认为简单的一个 this._events = {}
;就能够了,可是经过jsperf
(一个小彩蛋,有须要的搜如下,查看性能工具) 比较二者的性能,源码中高了不少,我就不具体一一讲解了,附上源码地址,有兴趣的能够去学习
lib/events源码地址 https://github.com/nodejs/nod...
源码篇幅过长,给了地址能够对比继续研究,毕竟是公众号文章,不想被说。可是一些疑问仍是要讲的,嘿嘿。
看一段代码:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter{}; const myEmitter = new MyEmitter(); myEmitter.on('event', function() { console.log('listener1'); }); myEmitter.on('event', async function() { console.log('listener2'); setTimeout(() => { console.log('我是异步中的输出'); resolve(1); }, 1000); }); myEmitter.on('event', function() { console.log('listener3'); }); myEmitter.emit('event'); console.log('end');
输出结果以下:
// 输出结果 listener1 listener2 listener3 end 我是异步中的输出
EventEmitter触发事件的时候,各监听函数的调用
是同步的(注意:监听函数的调用是同步的,'end'的输出在最后),可是并非说监听函数里不能包含异步的代码,代码中listener2那个事件就加了一个异步的函数,它是最后输出的。
我为何要把这个单独写成一个小标题来说,由于发现网上好多文章都是错的,或者不明确,给你们形成了误导。
看这里,某API网站的一段话,具体网站名称在这里就不说了,不想招黑,这段内容没问题,可是对于刚接触事件机制的小伙伴容易混淆
以fs.open
为例子,看一下到底何时产生了事件,何时触发,和EventEmitter有什么关系呢?
流程的一个说明:本图中详细绘制了从 异步调用开始--->异步调用请求封装--->请求对象传入I/O线程池完成I/O操做--->将完成的I/O结果交给I/O观察者--->从I/O观察者中取出回调函数和结果调用执行。
关于事件你看图中第三部分,事件循环那里。Node.js 全部的异步 I/O 操做(net.Server, fs.readStream 等)在完成后
都会添加一个事件到事件循环的事件队列中。
事件的触发,咱们只须要关注图中第三部分,事件循环会在事件队列中取出事件处理。fs.open
产生事件的对象都是 events.EventEmitter 的实例,继承了EventEmitter,从事件循环取出事件的时候,触发这个事件和回调函数。
越写越多,越写越想,老是这样,须要控制一下。
当咱们直接为EventEmitter定义一个error事件,它包含了错误的语义,咱们在遇到 异常的时候一般会触发 error 事件。
当 error 被触发时,EventEmitter 规定若是没有响 应的监听器,Node.js 会把它看成异常,退出程序并输出错误信息。
var events = require('events'); var emitter = new events.EventEmitter(); emitter.emit('error');
运行时会报错
node.js:201 throw e; // process.nextTick error, or 'error' event on first tick ^ Error: Uncaught, unspecified 'error' event. at EventEmitter.emit (events.js:50:15) at Object.<anonymous> (/home/byvoid/error.js:5:9) at Module._compile (module.js:441:26) at Object..js (module.js:459:10) at Module.load (module.js:348:31) at Function._load (module.js:308:12) at Array.0 (module.js:479:10) at EventEmitter._tickCallback (node.js:192:40)
咱们通常要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。
默认状况下针对单一事件的最大listener数量是10,若是超过10个的话listener仍是会执行,只是控制台会有警告信息,告警信息里面已经提示了操做建议,能够经过调用emitter.setMaxListeners()来调整最大listener的限制
(node:9379) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit
上面的警告信息的粒度不够,并不能告诉咱们是哪里的代码出了问题,能够经过process.on('warning')来得到更具体的信息(emitter、event、eventCount)
process.on('warning', (e) => { console.log(e); }) { MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:289:19) at MyEmitter.prependListener (events.js:313:14) at Object.<anonymous> (/Users/xiji/workspace/learn/event-emitter/b.js:34:11) at Module._compile (module.js:641:30) at Object.Module._extensions..js (module.js:652:10) at Module.load (module.js:560:32) at tryModuleLoad (module.js:503:12) at Function.Module._load (module.js:495:3) at Function.Module.runMain (module.js:682:10) at startup (bootstrap_node.js:191:16) name: 'MaxListenersExceededWarning', emitter: MyEmitter { domain: null, _events: { event: [Array] }, _eventsCount: 1, _maxListeners: undefined }, type: 'event', count: 11 }
好比fs
模块 net
模块
观察者模式与发布-订阅者模式,在平时你能够认为他们是一个东西,可是在某些场合(好比面试)可能须要稍加注意,看一下两者的区别对比
借用网上的一张图
从图中能够看出,发布-订阅模式中间包含一个Event Channel
参考文章:
加入咱们一块儿学习吧!