[原] 探索 EventEmitter 在 Node.js 中的实现

3,627 阅读4分钟

你有没有想过,为什么浏览器的 div 上可以绑定多个 onclick 事件,点击一下 div 可以触发全部的事件,jquery 的 .on().off()one() 又是如何实现的?Node.js 事件驱动的原理是怎样的?

实际上这一切都是 EventEmitter 在背后做支持,它是 JavaScript 经典的事件驱动实现,现在我们来看下 Node.js 中是如何实现的。

本文所说的监听事件在实现上都为函数,读者可以认为两者相等以方便阅读。

因为原来的 Node 代码量比较多,为了方便演示,作者把本文的源代码示例中涉及数据验证,错误处理的部分删除,保留了主要内容。

准备:

概览 EventEmitter

内部属性:

  • _events:用来存储监听事件,可以是一个事件或事件数组。
  • _eventsCount:记录已注册的监听事件个数。

主要方法:

  • emitter.addListener/on(eventName, listener) 添加类型为 eventName 的监听事件到事件数组尾部
  • emitter.prependListener(eventName, listener) 添加类型为 eventName 的监听事件到事件数组头部
  • emitter.emit(eventName[, ...args]) 触发类型为 eventName 的监听事件
  • emitter.removeListener/off(eventName, listener) 移除类型为 eventName 的监听事件
  • emitter.once(eventName, listener) 添加类型为 eventName 的监听事件,以后只能执行一次并删除
  • emitter.removeAllListeners([eventName]) 移除全部类型为 eventName 的监听事件

正文

1. 初始化 init

_events 不存在时,使用 Object.create(null) 来初始化,并把 _eventsCount 设 0。

划重点 —— Object.create(null) 可以创建一个没有原型的对象

为什么要用这种方法创建对象呢?开发者这么做的目的其实还是出于性能上的考虑,因为 EventEmitter 在 Node.js 中应用广泛,为节省服务器内存和执行速度上不必要的开销,肯定能省则省呗。

2. 添加事件绑定 addListener

首先判断 target_events 是否存在,如果不存在则还是用 Object.create(null) 创建。

如果存在,触发 newListener 类型的事件。然后通过 event[type] 找到已经注册 type 类型的监听事件/监听事件数组,并存到 existing 中。

如果该事件值为 undefined,则把直接把要注册的监听事件 listener 赋给不存在的事件。否则,更新事件数组(单一的 listener 要转为数组)。

注意 prepend 的使用,可以灵活地把 listener 添加到监听函数数组头部或尾部。

3. 事件添加到数组头部 prependListener

和 addListener 类似,但是 prependtrue

2. 触发事件 emit

若 handler 不存在,直接返回 false。

若 handler 是一个函数,使用 Reflect 调用函数。如果是数组的话则遍历数组并调用,然后返回 true。

3 移除事件绑定 removeListener

type 取出要删除的监听函数列表 list = event[type],当 list 等于要删除的监听函数时,_eventsCount 减一后如果为 0,直接初始化 _events,否则只删除当前类型的监听函数。

接着往下看,若 typeof list !== 'function'list 为数组时,先确定要删除监听事件的位置 position,然后删掉对应的函数。

注意:为什么不用 list.splice(postion, 1) 而要专门写一个 spliceOne 来删除呢?

因为这个两参数的方法要比内置的 splice 可能快上 1.5 - 10 倍!我专门查看了下提交记录,这个版本的方法经过几个开发者改动过最终成为现在这个样子。不得不佩服各路大神对开源的贡献!至于 splice 为什么慢,我没能查到原因,也许需要去看 v8 源码。

4 事件只能执行一次 once

这个方法的实现有点 tricky,为了维护 fired 的状态它用到了闭包

其它还有一些方法,我不再多写了,基本上原理就是这样。有兴趣的同学可以自己点击前文的源码链接查看。

下面是我抄 Node.js 的 EventEmitter 简单代码实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(type, handler) {
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(handler);
  }

  off(type, handler) {
    if (!this.events[type]) {
      return;
    }
    this.events[type] = this.events[type].filter(item => item !== handler);
  }

  emit(type, ...args) {
    this.events[type].forEach((item) => {
      Reflect.apply(item, this, args);
    });
  }

  once(type, handler) {
    this.on(type, this._onceWrap(type, handler, this));
  }

  _onceWrap(type, handler, target) {
    const state = { fired: false, handler, type , target};
    const wrapFn = this._onceWrapper.bind(state);
    state.wrapFn = wrapFn;
    return wrapFn;
  }

  _onceWrapper(...args) {
    if (!this.fired) {
      this.fired = true;
      Reflect.apply(this.handler, this.target, args);
      this.target.off(this.type, this.wrapFn);
    }
  }
}
// 初始化
const ee = new EventEmitter();

// 注册所有事件
ee.once('wakeUp', (name) => { console.log(`${name}起来啦`); });
ee.on('eat', (name) => { console.log(`${name}吃馒头啦`) });
ee.on('eat', (name) => { console.log(`${name}喝水啦`) });
const meetingFn = (name) => { console.log(`${name}开早会啦`) };
ee.on('work', meetingFn);
ee.on('work', (name) => { console.log(`${name}码代码啦`) });

ee.emit('wakeUp', '子非');
ee.emit('wakeUp', '子非');         // 第二次没有触发
ee.emit('eat', '子非');
ee.emit('work', '子非');
ee.off('work', meetingFn);        // 移除开会事件
ee.emit('work', '子非');           // 再次工作


输出:
子非起来啦
子非吃馒头啦
子非喝水啦
子非开早会啦
子非码代码啦
子非码代码啦

总结:

读完 Node.js 的 EventEmitter 实现,一些细节上的处理我觉得非常棒,而设计层面上,优秀的包装和抽象思路也让我觉得十分经典。EventEmitter 非常重要,很多大型库像 Webpack,Socket.io 都是基于它来实现的,对于学习 Js 的同学来说是必须掌握它的。

欢迎沟通评论和交流!!!如果这篇文章帮助到了你,麻烦给个小心心哦❤️❤️❤️