sjn_hoho

sjn_hoho

我读源码之events

前言

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

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

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 怎么实现的?

正文(源码地址

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

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方法

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 方法。

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 请自行搜索

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 生态有了更好的理解。