Events 是 Node.js 中最重要的核心模块之一,很多模块都是依赖其创建的,例如[上一节分析的流](https://www.cnblogs.com/strick/p/16225418.html),文件、网络等模块。
  比较知名的 Express、KOA 等框架在其内部也使用了 Events 模块。
  Events 模块提供了[EventEmitter](https://nodejs.org/dist/latest-v18.x/docs/api/events.html)类,EventEmitter 也叫事件触发器,是一种观察者模式的实现。
  观察者模式是软件设计模式的一种,在此模式中,一个目标对象(即被观察者对象)管理所有依赖于它的观察者对象。
  当其自身状态发生变化时,将以广播的方式主动发送通知(在通知中可携带一些数据),这样就能在两者之间建立触发机制,达到解耦地目的。
  与浏览器中的事件处理器不同,在 Node.js 中没有捕获、冒泡、preventDefault() 等概念或方法。
  本系列所有的示例源码都已上传至Github,[点击此处](https://github.com/pwstrick/node)获取。
## 一、方法原理
  在下面的示例中,加载 events 模块,实例化 EventEmitter 类,赋值给 demo 变量,声明 listener() 监听函数。
  然后调用 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');
~~~
  若要移除监听函数,可以像下面这样,注意,off() 方法不是移除事件,而是函数。
~~~
demo.off('begin', listener);
~~~
**1)构造函数**
  在[src/lib/events.js](https://github.com/nodejs/node/blob/master/lib/events.js)文件中,可以看到构造函数的源码,它会调用 init() 方法,并指定 this,也就是当前实例。
~~~
function EventEmitter(opts) {
EventEmitter.init.call(this, opts);
}
~~~
  删减了 init() 方法源码,只列出了关键部分,当 \_events 私有属性不存在时,就通过 ObjectCreate(null) 创建。
  之所以使用 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()**
  on() 其实是 addListener() 的别名,具体逻辑在 \_addListener() 函数中。
~~~
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
~~~
  在 \_addListener() 函数中,会对传入的事件判断之前是否注册过。
  如果之前未注册过,那么就在键值对中注册新的事件和监听函数。
  如果之前已注册过,那么就将多个监听函数合并成数组使用,在触发时会依次执行。
  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;
}
~~~
  在下面这个示例中,同一个事件,注册了两个监听函数,在触发时,会先打印“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');
~~~
  EventEmitter 还提供了一个 once() 方法,也是用于注册事件,但只会触发一次。
**3)off()**
  off() 方法是 removeListener() 的别名。
~~~
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
~~~
  下面是删减过的 removeListener() 方法源码,先是读取指定事件的监听函数赋值给 list 变量,类型是函数或数组。
  如果要移除的事件与 list 匹配,当只剩下一个事件时,就赋值 ObjectCreate(null);否则使用 delete 关键字删除键值对的属性。
  如果 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;
};
~~~
  Node.js 没有使用 splice() 方法,而是选择了一个最小可用的函数,据说性能有所提升。
  spliceOne() 函数很简单,如下所示,从指定索引加一的位置开始循环,后一个元素向前搬移到上一个元素的位置,再将最后那个元素移除。
~~~
function spliceOne(list, index) {
for (; index + 1 < list.length; index++)
list[index] = list[index + 1];
list.pop();
}
~~~
**4)emit()**
  下面是删减过的 emit() 方法源码,首先读取监听函数并赋值给 handler。
  若 handler 是函数,则直接通过 apply() 运行。
  若 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;
};
~~~
  arrayClone() 函数的作用是防止在 emit 时移除事件对其进行干扰,在函数中使用 switch 分支和数组的 slice() 方法。
  官方说从 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)同步**
  官方明确指出 EventEmitter 是按照注册的顺序同步地调用所有监听函数,避免竞争条件和逻辑错误。
  在适当的时候,监听函数可以使用 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)循环**
  先来看第一个循环的示例,在注册的 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'); // 报错
~~~
  再看看第二个循环的示例,在注册的 loop 事件中,又注册了一次 loop 事件,这么处理并不会报错,因为只是多注册了一次同名事件而已。
~~~
const listener = () => {
console.log('strick');
};
demo.on('loop', () => {
demo.on('loop', listener);
listener();
});
demo.emit('loop'); // strick
demo.emit('loop'); // strick strick
~~~
  在每次触发时,打印的数量要比上一次多一个。
**3)错误处理**
  在下面这个示例中,由于没有注册 error 事件,因此只要一触发 error 事件就会抛出错误,后面的打印也不会执行。
~~~
const EventEmitter = require('events');
const demo = new EventEmitter();
demo.emit('error', new Error('error'));
console.log('strick');
~~~
  将代码做下调整,为了防止 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 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020