盒子
盒子

我读源码之events

前言

events模块一直是Node.js源码解读界的“网红”,许多博客和文章珠玉在前,但看着别人的思路一步步来总是不过瘾,这次试着自己读一读。漫无目的的阅读很可能看过就忘,所以先提2个问题给自己。

问题一:下面这段代码为什么不会死循环?

1
2
3
4
5
6
7
const EventEmitter = require('events');
let emitter = new EventEmitter();
emitter.on('myEvent', function sth () {
emitter.on('myEvent', sth);
console.log('hi');
});
emitter.emit('myEvent');

问题二:event.once怎么实现的?

正文(源码地址

构造函数中初始化了几个相关变量。

1
2
3
4
5
6
7
8
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;//最大监听者数量
};

在导出依赖时发现有一句
EventEmitter.EventEmitter = EventEmitter;
终于搞明白为啥经常看到一些库中的用法是
var eventEmitter = require('events').EventEmitter
原来是一样的,只是历史遗留问题。
然后是on方法,on的实质是一个function(type,listener)
on方法源码也比较简单,处理了一些错误情况和可能引起的问题,最后将.events[type]的listener数组中加上新的listener,或者在没有的已存在的情况不创建数组直接赋值。值得注意是如果已存在的监听中有’newListener’的监听者,则需要先发送一个’newListener’事件。
接下来就是emit方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
EventEmitter.prototype.emit = function emit(type, ...args) {
let doError = (type === 'error');
const events = this._events;
if (events !== undefined)
doError = (doError && events.error === undefined);
//是error事件,而且有对应的listener,为false
else if (!doError)//不是error事件,但是没有listener,直接返回啊
return false;
//...
//错误处理,events里没有error的listener直接throw

const handler = events[type];

if (handler === undefined)
return false;

if (typeof handler === 'function') {
//只有一个监听器的情况下,直接调用,对应前面on的时候第一个直接赋值
Reflect.apply(handler, this, args);
} else {
//数组情况下,直接clone一个数组,循环处理
const len = handler.length;
const listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
Reflect.apply(listeners[i], this, args);
}
return true;
};

看了emit方法后,就可以解释前文的第一个问题,当myEvent被emit后其实events[‘myEvent’]最后仅是创建了监听器数组。发现网上这个问题的几乎所有的答案都是else里的原因,通过数组clone了一个数组,没有操作原数组,所以不会出现死循环。看来真理还是掌握在少数人手里。
接下来看一下once方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function onceWrapper(...args) {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
Reflect.apply(this.listener, this.target, args);
}
}

function _onceWrap(target, type, listener) {
var state = { fired: false, wrapFn: undefined, target, type, listener };
var wrapped = onceWrapper.bind(state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}

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;
};

其实就是通过闭包实现了一个名为fired的flag的私有,事件第一次被触发时,先调用removeListener,然后在运行对应的操作函数,并将fired设为true,下次就再也无法触发了,而且因为remove了这个监听器,所以之前的闭包也被回收了。
在once中的removeListener又是怎么实现的呢?
算了,不贴源码了,感觉很简单,就是将events[‘type’]中对应删掉,Object就delete,数组就spliceOne。其中这个spliceOne性能比Array自带的splice好很多。具体benchmark请自行搜索

1
2
3
4
5
function spliceOne(list, index) {
for (; index + 1 < list.length; index++)
list[index] = list[index + 1];
list.pop();
}

结尾

events是Node.js的核心模块,Stream就是给予events实现的,其他很多模块又是基于Stream实现的,源码也比较简单,很容易理解,直接阅读源码也不会人云亦云地回答问题,对整个Node.js生态有了更好的理解。