你有没有想过,为什么浏览器的 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 类似,但是prepend
为
true
。
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 的同学来说是必须掌握它的。
欢迎沟通评论和交流!!!如果这篇文章帮助到了你,麻烦给个小心心哦❤️❤️❤️