本次尝试浅析Node.js中的EventEmitter模块的事件机制,分析在Node.js中实现发布订阅模式的一些细节。完整Node.js源码点这里。html
欢迎关注个人博客,不按期更新中——node
大多数 Node.js 核心 API 都采用惯用的异步事件驱动架构,其中某些类型的对象(触发器)会周期性地触发命名事件来调用函数对象(监听器)。例如,net.Server 对象会在每次有新链接时触发事件;fs.ReadStream 会在文件被打开时触发事件;流对象 会在数据可读时触发事件。全部能触发事件的对象都是 EventEmitter 类的实例。
Node.js中对EventEmitter类的实例的运用能够说是贯穿整个Node.js,相信这一点你们已是很熟悉的了。其中所运用到的发布订阅模式,则是很经典的管理消息分发的一种方式。在这种模式中,发布消息的一方不须要知道这个消息会给谁,而订阅的一方也无需知道消息的来源。使用方式通常以下:git
const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('触发了一个事件A!'); }); myEmitter.emit('event'); //触发了一个事件A!
当咱们订阅了'event'事件后,能够在任何地方经过emit('event')
来执行事件回调,EventEmitter至关于一个中介,负责记录都订阅了哪些事件而且触发后的回调是什么,当事件被触发,就将回调一一执行。github
从源码中看下EventEmitter类的是如何实现发布订阅的。
首先咱们梳理一下实现这个模式须要的步骤:chrome
在生成空对象的方式中,通常容易想到的是直接进行赋值空对象即 var a = {};
,Node.js中采用的方式为var a = Object.create(null)
,使用这种方式理论上是应该对对象的属性存取的操做更快,出于好奇做者对这两种方式作了个粗略的对比:api
var a = {} a.test = 1 var b = Object.create(null) b.test = 1 console.time('{}') for(var i = 0; i < 1000; i++) { console.log(a.test) } console.timeEnd('{}') console.time('create') for(var i = 0; i < 1000; i++) { console.log(b.test) } console.timeEnd('create')
打印结果显示出来貌似直接用空对象赋值与经过Object.create的方式并无很大的性能差别,而且尚未谁必定占了上风,就目前该空对象用来存储注册的监听事件与回调来看,若是直接用{}来初始化this._events性能方面影响也许不大。不过这一点只是我的观点,暂时还并不能领会Node里面如此运用的深意。数组
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener;
添加监听者的方法为addListener,同时on是其别名。浏览器
if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } ... }
若是以前不存在监听事件,则会进入第一个判断内,其中type为事件类型,listener为触发的事件回调。若是以前注册过事件,那么回调函数会添加到回调队列的头或尾。看以下打印结果:架构
myEmitter.on('event', () => { console.log('触发了一个事件A!'); }); myEmitter.on('event', () => { console.log('触发了一个事件B!'); }); myEmitter.on('talk', () => { console.log('触发了一个事件CS!'); // myEmitter.emit('talk'); }); console.log(myEmitter._events) //{ event: [ [Function], [Function] ], talk: [Function] }
myEmitter实例的_events方法就是咱们存储事件与回调的对象,能够看到当咱们依次注册事件后,回调会被推到 _events对应key的value中。app
在触发的emit函数中,会根据触发时传入参数的多少执行不一样的函数:(参数不一样直接执行不一样的函数,这个操做应该会让性能更好,不过做者没有测试这点)
switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); }
以emitMany为例看下内部触发实现:
var isFn = typeof handler === 'function'; function emitMany(handler, isFn, self, args) { if (isFn) //handler类型为函数,即对这个事件只注册了一个监听函数 handler.apply(self, args); else { //当对同一事件注册了多个监听函数的时候,handler类型为数组 var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } function arrayClone(arr, n) { var copy = new Array(n); for (var i = 0; i < n; ++i) copy[i] = arr[i]; return copy; }
源码中实现了arrayClone
方法,来复制一份一样的监听函数,再去依次执行副本。我的对这个作法的理解是,当触发当前类型事件后,就锁定须要执行的回调函数队列,不然当触发回调过程当中,再去推入新的回调函数,或者删除已有回调函数,容易形成不可预知的问题。
若是回调事件只有一个那么直接删除便可,若是是数组就像以前看到的那样注册了多组对一样事件的监听,就要涉及从数组中删除项的实现。在这里Node本身实现了一个spliceOne函数来代替原生的splice,而且说明其方式比splice快1.5倍。下面是做者进行的简易粗略,不严谨的运行时间比较:
上面作了一个很粗略的运算时间比较,一样是对长度为1000的数组第100项进行删除操做,而且代码运行在chrome浏览器下(版本号61.0.3163.100)node源码中本身实现的方法确实比原生的splice快了一些,不过结果只是一个参考毕竟这个对比很粗略,有兴趣的童鞋能够写一组benchmark来进行对比。
源码的边界状况比较多。在这里只作一个相对简单的流程浅析,哪里说明有误欢迎指正~
PS:相关实例源码:https://github.com/Aaaaaaaty/...
惯例po做者的博客,不定时更新中——有问题欢迎在issues下交流。