深入出不来nodejs源码-events模块
这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼。
学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定。这个东西贯穿了整个JS的学习过程,无论是刚开始的自己获取DOM手动绑,还是后期vue的直接@click,所有的交互都离不开这个东西。
同样,在node中,事件绑定也贯穿了整个框架。基本上大多数的内置模块以events为原型,下面的代码随处可见:
EventEmitter.call(this);
不同的是,页面上DOM的事件绑定是由浏览器来实现,触发也是一些操作'间接'触发,并不需要去主动emit对应事件,并且有冒泡和捕获这两特殊的性质。
但是在node中,不存在dom,绑定的目标是一个对象(dom本质上也是对象),在内部node自己用纯JS实现了一个事件绑定与事件触发类。
本文相关源码来源于https://github.com/nodejs/node/blob/master/lib/events.js。
首先看一下构造函数:
function EventEmitter() { EventEmitter.init.call(this); }
这里会调用一个init方法,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; };
涉及的三个属性分别是:
1、_events => 一个挂载属性,空对象,负责收集所有类型的事件
2、_eventsCount => 记录目前绑定事件类型的数量
3、_maxListeners => 同类型事件listener数量限制
事件相关的主要操作有3个,依次来看。
绑定事件/on
虽然一般用的AP都是event.on,但是其实用addListener是一样的:
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); };
这个addListener跟DOM的addEventListener稍微有点不一样,前两个参数一致,分别代表类型、回调函数。
但是最后一个参数,这里代表的是否优先插入该事件,有一个方法就是做这个的:
EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); };
最终都指向这个_addListener,分步解释:
/** * 事件绑定方法 * @param {Object} target 目标对象 * @param {String} type 事件名称 * @param {Function} listener 回调函数 * @param {Boolean} prepend 是否插入 */ function _addListener(target, type, listener, prepend) { // 指定事件类型的回调函数数量 var m; // 事件属性对象 var events; // 对应类型的回调函数 var existing; if (typeof listener !== 'function') { const errors = lazyErrors(); throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener); } // 尝试获取对应类型的事件 events = target._events; // 未找到对应的事件相关属性 if (events === undefined) { events = target._events = Object.create(null); target._eventsCount = 0; } // 当存在对象的事件属性对象时 else {} // more... return target; }
这里首先会尝试获取指定对象的_events属性,即构造函数中初始化的挂载对象属性。
由于无论是任意构造函数中调用EventEmitter.call(this)或者new EventEmitter()都会在生成对象上挂载一个_events对象,所以这个判断暂时找不到反例。
当不存在就手动初始化一个,并添加一个记数属性重置为0。
当存在时,处理代码如下:
events = target._events; if (events === undefined) { // ... } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener !== undefined) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } // 尝试获取对应类型的回调函数集合 existing = events[type]; }
这个地方的注释主要讲的是,当绑定了type为newListener的事件时,每次都会触发一次这个事件,如果再次绑定该事件会出现递归问题。所以要判断是否存在newListener事件类型,如果有就先触发一次newListener事件。
先不管这个,最后会尝试获取指定类型的事件listener容器,下面就是对existing的处理。
// 首次添加该类型事件时 if (existing === undefined) { // 直接把函数赋值给对应类型的key existing = events[type] = listener; // 记数+1 ++target._eventsCount; } else { // 1.已有对应类型 但是只有一个 if (typeof existing === 'function') { // 转换数组 根据prepend参数安排顺序 existing = events[type] = prepend ? [listener, existing] : [existing, listener]; // If we've already got an array, just append. } // 2.已有多个 判断是否有优先的flag进行前插或后插 else if (prepend) { existing.unshift(listener); } else { existing.push(listener); } // Check for listener leak // ... }
这里的处理就能很清楚的看到events模块对于事件绑定的处理,_events相当于一个总对象,属性的key就是对应的事件类型type,而key对应的value就是对应的listener。只有一个时,就直接用该listener做值。重复绑定同类型的事件,这时值会转换为数组保存所有的listener。这里prepend就是之前的最后一个参数,允许函数插入到队列的前面,优先触发。
最后还有一个绑定事件的数量判断:
// 获取_maxListeners参数 同类型事件listener最大绑定数量 m = $getMaxListeners(target); // 如果超出就发出可能有内存泄漏的警告 if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; // 因为是warning所以不会有error code 可以不理这个东西 // eslint-disable-next-line no-restricted-syntax const w = new Error('Possible EventEmitter memory leak detected. ' + `${existing.length} ${String(type)} listeners ` + 'added. Use emitter.setMaxListeners() to ' + 'increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; process.emitWarning(w); }
看看就好,程序员不用管warning,哈哈。
一次绑定事件/once
有些时候希望事件只触发一次,原生的API目前不存在该功能,当初jquery也是封装了一个once方法,对应的这个events模块也有。
EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') { const errors = lazyErrors(); throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener); } this.on(type, _onceWrap(this, type, listener)); return this; };
除去那个判断,其实绑定的方法还是同一个,只是对应的listener变成了一个包装函数,来看看。
function _onceWrap(target, type, listener) { // this绑定对象 var state = { fired: false, wrapFn: undefined, target, type, listener }; var wrapped = onceWrapper.bind(state); // 原生的listener挂载到这个包装函数上 wrapped.listener = listener; // 处理完后更新state属性 state.wrapFn = wrapped; // 返回的是一个包装的函数 return wrapped; } function onceWrapper(...args) { // 这里所有的this指向上面的state对象 // args来源于触发时候给的参数 if (!this.fired) { // 解绑该包装后的listener this.target.removeListener(this.type, this.wrapFn); this.fired = true; // 触发listener Reflect.apply(this.listener, this.target, args); } }
思路其实跟jquery的源码差不多,也是包装listener,当触发一次事件时,先解绑这个listener再触发事件。
需要注意的是,这里存在两个listener,一个是原生的,一个是包装后的。绑定的是包装的,所以解绑的第二个参数也要是包装的。其中原生的作为listener属性挂载到包装后的函数上,实际上触发包装listener后内部会隐式调用原生listener。
事件触发/emit
看完绑定,来看触发。
EventEmitter.prototype.emit = function emit(type, ...args) { let doError = (type === 'error'); const events = this._events; // 判断是否触发的error类型事件 if (events !== undefined) doError = (doError && events.error === undefined); else if (!doError) return false; // If there is no 'error' event listener then throw. if (doError) { // 错误处理 不看 } // 跟之前的existing一个东西 const handler = events[type]; if (handler === undefined) return false; // 如果只有一个 直接调用 if (typeof handler === 'function') { Reflect.apply(handler, this, args); } else { // 多个listener 依次触发 const len = handler.length; const listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) Reflect.apply(listeners[i], this, args); } return true; };
太简单了,懒得解释。
事件解绑/removeListener
同样分几步来看解绑的过程,首先是参数声明:
// Emits a 'removeListener' event if and only if the listener was removed. EventEmitter.prototype.removeListener = function removeListener(type, listener) { // list => listener容器 // events => 事件根对象 // position => 记录删除listener位置 // i => 迭代参数 // originalListener => 原生listener 参考上面的once var list, events, position, i, originalListener; if (typeof listener !== 'function') { const errors = lazyErrors(); throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener); } events = this._events; if (events === undefined) return this; list = events[type]; if (list === undefined) return this; // ... }
比较简单,每个参数的用处都很明显,错误判断后,下面有两种不同的情况。
当对应type的listener只有一个时:
EventEmitter.prototype.removeListener = function removeListener(type, listener) { // list => listener容器 // events => 事件根对象 // position => 记录删除listener位置 // i => 迭代参数 // originalListener => 原生listener 参考上面的once var list, events, position, i, originalListener; // ... // listener只有一个的情况 if (list === listener || list.listener === listener) { // 如果一个绑定事件都没了 直接重置_events对象 if (--this._eventsCount === 0) this._events = Object.create(null); else { // 删除对应的事件类型 delete events[type]; // 尝试触发一次removeListener事件 if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { // ... } return this; };
这里还分了两种情况,如果_eventsCount为0,即所有的type都被清完,会重置_events对象。
理论上来说,按照else分支的逻辑,当listener剩一个的时候都是直接delete对应的key,最后剩下的还是一个空对象,那这里的重重置似乎变得没有意义了。
我猜测估计是为了V8层面的优化,因为对象的属性在破坏性变动(添加属性、重复绑定同type事件导致函数变成函数数组)的时候,所需的内存会进行扩充,这个过程是不可逆的,就算最后只剩一个空壳对象,其实际占用也是相当大的。所以为了省空间,这里进行重置,用很小的空间初始化_events对象,原来的空间被回收。
当对应type的listener为多个时,就要遍历了。
if (list === listener || list.listener === listener) { // ... } else if (typeof list !== 'function') { position = -1; // 倒序遍历 for (i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { // once绑定的事件有listener属性 originalListener = list[i].listener; // 记录位置 position = i; break; } } if (position < 0) return this; // 在第一个位置时 if (position === 0) list.shift(); else { // 删除数组对应索引的值 if (spliceOne === undefined) spliceOne = require('internal/util').spliceOne; spliceOne(list, position); } // 如果数组里只有一个值 转换为单个值 // 有点像HashMap的链表-红黑树转换…… if (list.length === 1) events[type] = list[0]; // 尝试触发removeListener if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener); }
太简单了,自己看吧。
其他还有诸如removeAllListeners、_listeners、eventNames等API,有兴趣的可以自行去看。