企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
&emsp;&emsp;Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如[上一节分析的流](https://www.cnblogs.com/strick/p/16225418.html),文件、网络等模块。 &emsp;&emsp;比较知名的 Express、KOA 等框架在其内部也使用了 Events 模块。 &emsp;&emsp;Events 模块提供了[EventEmitter](https://nodejs.org/dist/latest-v18.x/docs/api/events.html)类,EventEmitter 也叫事件触发器,是一种观察者模式的实现。 &emsp;&emsp;观察者模式是软件设计模式的一种,在此模式中,一个目标对象(即被观察者对象)管理所有依赖于它的观察者对象。 &emsp;&emsp;当其自身状态发生变化时,将以广播的方式主动发送通知(在通知中可携带一些数据),这样就能在两者之间建立触发机制,达到解耦地目的。 &emsp;&emsp;与浏览器中的事件处理器不同,在 Node.js 中没有捕获、冒泡、preventDefault() 等概念或方法。 &emsp;&emsp;本系列所有的示例源码都已上传至Github,[点击此处](https://github.com/pwstrick/node)获取。 ## 一、方法原理 &emsp;&emsp;在下面的示例中,加载 events 模块,实例化 EventEmitter 类,赋值给 demo 变量,声明 listener() 监听函数。 &emsp;&emsp;然后调用 demo 的 on() 方法注册 begin 事件,最后调用 emit() 触发 begin 事件,在控制台打印出“strick”。 ~~~ const EventEmitter = require('events'); const demo = new EventEmitter(); const listener = () => { // 监听函数 console.log('strick'); }; // 注册 demo.on('begin', listener); demo.emit('begin'); ~~~ &emsp;&emsp;若要移除监听函数,可以像下面这样,注意,off() 方法不是移除事件,而是函数。 ~~~ demo.off('begin', listener); ~~~ **1)构造函数** &emsp;&emsp;在[src/lib/events.js](https://github.com/nodejs/node/blob/master/lib/events.js)文件中,可以看到构造函数的源码,它会调用 init() 方法,并指定 this,也就是当前实例。 ~~~ function EventEmitter(opts) { EventEmitter.init.call(this, opts); } ~~~ &emsp;&emsp;删减了 init() 方法源码,只列出了关键部分,当 \_events 私有属性不存在时,就通过 ObjectCreate(null) 创建。 &emsp;&emsp;之所以使用 ObjectCreate(null) 是为了得到一个不继承任何原型方法的干净键值对。\_events 的 key 是事件名称,value 是监听函数。 ~~~ EventEmitter.init = function(opts) { // 当 _events 私有属性不存在时 if (this._events === undefined || this._events === ObjectGetPrototypeOf(this)._events) { this._events = ObjectCreate(null); // 不继承任何原型方法的干净键值对 this._eventsCount = 0; } }; ~~~ **2)on()** &emsp;&emsp;on() 其实是 addListener() 的别名,具体逻辑在 \_addListener() 函数中。 ~~~ EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; ~~~ &emsp;&emsp;在 \_addListener() 函数中,会对传入的事件判断之前是否注册过。 &emsp;&emsp;如果之前未注册过,那么就在键值对中注册新的事件和监听函数。 &emsp;&emsp;如果之前已注册过,那么就将多个监听函数合并成数组使用,在触发时会依次执行。 &emsp;&emsp;EventEmitter 默认的事件最大监听数是 10,若注册的数量超出了这个限制,那么就会发出警告,不过事件仍然可以正常触发。 ~~~ function _addListener(target, type, listener, prepend) { let m; let events; let existing; events = target._events; // 判断传入的事件是否注册过 if (events === undefined) { events = target._events = ObjectCreate(null); target._eventsCount = 0; } else { existing = events[type]; } // 在键值对中注册新的事件和监听函数 if (existing === undefined) { events[type] = listener; ++target._eventsCount; } else { // 已存在相同名称的事件 // 添加第二个相同名称的事件时,将 events[type] 修改成数组 if (typeof existing === "function") { existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else if (prepend) { existing.unshift(listener); } else { // 若是数组,就添加到末尾 existing.push(listener); } // 读取最大事件监听数 m = _getMaxListeners(target); if (m > 0 && existing.length > m && !existing.warned) { existing.warned = true; const w = genericNodeError( `Possible EventEmitter memory leak detected. ${existing.length} ${String(type)} listeners ` + `added to ${inspect(target, { depth: -1 })}. Use emitter.setMaxListeners() to increase limit`, { name: 'MaxListenersExceededWarning', emitter: target, type: type, count: existing.length }); process.emitWarning(w); } } return target; } ~~~ &emsp;&emsp;在下面这个示例中,同一个事件,注册了两个监听函数,在触发时,会先打印“strick”,再打印“freedom”。 ~~~ const EventEmitter = require('events'); const demo = new EventEmitter(); const listener1 = () => { // 监听函数 console.log('strick'); }; const listener2 = () => { // 监听函数 console.log('freedom'); }; // 注册 demo.on('begin', listener1); demo.on('begin', listener2); demo.emit('begin'); ~~~ &emsp;&emsp;EventEmitter 还提供了一个 once() 方法,也是用于注册事件,但只会触发一次。 **3)off()** &emsp;&emsp;off() 方法是 removeListener() 的别名。 ~~~ EventEmitter.prototype.off = EventEmitter.prototype.removeListener; ~~~ &emsp;&emsp;下面是删减过的 removeListener() 方法源码,先是读取指定事件的监听函数赋值给 list 变量,类型是函数或数组。 &emsp;&emsp;如果要移除的事件与 list 匹配,当只剩下一个事件时,就赋值 ObjectCreate(null);否则使用 delete 关键字删除键值对的属性。 &emsp;&emsp;如果 list 是一个数组时,就遍历它,并记录匹配位置。若匹配位置在头部,就调用 shift() 方法移除,否则使用 splice() 方法。 ~~~ EventEmitter.prototype.removeListener = function removeListener(type, listener) { const events = this._events; // 读取指定事件的监听函数,类型是函数或数组 const list = events[type]; // 要移除的事件与 list 匹配 if (list === listener || list.listener === listener) { // 只剩下最后一个事件,就赋值 ObjectCreate(null) if (--this._eventsCount === 0) this._events = ObjectCreate(null); else { delete events[type]; // 删除键值对的属性 } } else if (typeof list !== "function") { let position = -1; // 遍历 list 数组,若查到匹配的就记录位置 for (let i = list.length - 1; i >= 0; i--) { if (list[i] === listener || list[i].listener === listener) { position = i; break; } } // 在头部就直接调用 shift() 方法 if (position === 0) list.shift(); else { if (spliceOne === undefined) spliceOne = require("internal/util").spliceOne; // 没有使用 splice() 方法,选择了一个最小可用的函数 spliceOne(list, position); } } return this; }; ~~~ &emsp;&emsp;Node.js 没有使用 splice() 方法,而是选择了一个最小可用的函数,据说性能有所提升。 &emsp;&emsp;spliceOne() 函数很简单,如下所示,从指定索引加一的位置开始循环,后一个元素向前搬移到上一个元素的位置,再将最后那个元素移除。 ~~~ function spliceOne(list, index) { for (; index + 1 < list.length; index++) list[index] = list[index + 1]; list.pop(); } ~~~ **4)emit()** &emsp;&emsp;下面是删减过的 emit() 方法源码,首先读取监听函数并赋值给 handler。 &emsp;&emsp;若 handler 是函数,则直接通过 apply() 运行。 &emsp;&emsp;若 handler 是数组,那么先调用 arrayClone() 函数将其克隆,在遍历数组,依次通过 apply() 运行。 ~~~ EventEmitter.prototype.emit = function emit(type, ...args) { const handler = events[type]; // 若 handler 是函数,则直接运行 if (typeof handler === 'function') { handler.apply(this, args); } else { const len = handler.length; // 数组克隆,防止在 emit 时移除事件对其进行干扰 const listeners = arrayClone(handler); // 遍历数组 for (let i = 0; i < len; ++i) { listeners[i].apply(this, args); } } return true; }; ~~~ &emsp;&emsp;arrayClone() 函数的作用是防止在 emit 时移除事件对其进行干扰,在函数中使用 switch 分支和数组的 slice() 方法。 &emsp;&emsp;官方说从 Node 版本 8.8.3 开始,这个实现要比简单地 for 循环快。 ~~~ function arrayClone(arr) { // 从 V8.8.3 开始,这个实现要比简单地 for 循环快 switch (arr.length) { case 2: return [arr[0], arr[1]]; case 3: return [arr[0], arr[1], arr[2]]; case 4: return [arr[0], arr[1], arr[2], arr[3]]; case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]]; case 6: return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; } // array.prototype.slice return ArrayPrototypeSlice(arr); } ~~~ ## 二、其他概念 **1)同步** &emsp;&emsp;官方明确指出 EventEmitter 是按照注册的顺序同步地调用所有监听函数,避免竞争条件和逻辑错误。 &emsp;&emsp;在适当的时候,监听函数可以使用 setImmediate() 或 process.nextTick() 方法切换到异步的操作模式,如下所示。 ~~~ const EventEmitter = require('events'); const demo = new EventEmitter(); demo.on('async', (a, b) => { setImmediate(() => { console.log(a, b); }); }); demo.emit('async', 'a', 'b'); ~~~ **2)循环** &emsp;&emsp;先来看第一个循环的示例,在注册的 loop 事件中,会不断地触发 loop 事件,那么最终会报栈溢出的错误。 ~~~ const EventEmitter = require('events'); const demo = new EventEmitter(); const listener = () => { console.log('strick'); }; demo.on('loop', () => { demo.emit('loop'); listener(); }); demo.emit('loop'); // 报错 ~~~ &emsp;&emsp;再看看第二个循环的示例,在注册的 loop 事件中,又注册了一次 loop 事件,这么处理并不会报错,因为只是多注册了一次同名事件而已。 ~~~ const listener = () => { console.log('strick'); }; demo.on('loop', () => { demo.on('loop', listener); listener(); }); demo.emit('loop'); // strick demo.emit('loop'); // strick strick ~~~ &emsp;&emsp;在每次触发时,打印的数量要比上一次多一个。 **3)错误处理** &emsp;&emsp;在下面这个示例中,由于没有注册 error 事件,因此只要一触发 error 事件就会抛出错误,后面的打印也不会执行。 ~~~ const EventEmitter = require('events'); const demo = new EventEmitter(); demo.emit('error', new Error('error')); console.log('strick'); ~~~ &emsp;&emsp;将代码做下调整,为了防止 Node.js 主线程崩溃,应该始终注册 error 事件,改造后,虽然也会报错,但是打印仍然能正常执行。 ~~~ demo.on('error', err => { console.error(err); }); demo.emit('error', new Error('error')); console.log('strick'); ~~~ 参考资料: [Node.js技术栈之事件触发器](https://www.nodejs.red/#/nodejs/events) [异步迭代器](https://mp.weixin.qq.com/s/PDCZ5FreFJDJDqpvOe3xKQ)  [饿了么事件异步面试题](https://github.com/ElemeFE/node-interview/tree/master/sections/zh-cn#%E4%BA%8B%E4%BB%B6%E5%BC%82%E6%AD%A5) [深入理解Node.js之Event](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter7/chapter7-1.html) [Node.js事件模块](http://nodejs.cn/learn/the-nodejs-events-module) [events事件模块](https://nodejs.org/dist/latest-v18.x/docs/api/events.html) [EventEmitter 源码分析与简易实现](https://segmentfault.com/a/1190000016654243) [源码分析:EventEmitter](https://juejin.cn/post/6969843023190425636) [详解Object.create(null)](https://juejin.cn/post/6844903589815517192) ***** > 原文出处: [博客园-Node.js精进](https://www.cnblogs.com/strick/category/2154090.html) [知乎专栏-前端性能精进](https://www.zhihu.com/column/c_1611672656142725120) 已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。