?
events 模块的运用贯穿整个 Node.JS, 读就 Vans 了.
1. 在使用层面有一个认识
1.1 Events 模块用于解决那些问题?
回调函数模式让 Node 可以处理异步操作, 但是, 为了适应回调函数, 异步操作只能有两个状态: 开始和结束. 对于那些多状态的异步操作(状态 1, 状态 2, 状态 3, ....), 回调函数就会无法处理. 这是就必须将异步操作拆开, 分成多个阶段, 每个阶段结束时, 调用回调函数.
为了解决这个问题, Node 提供了 EventEmitter 接口. 通过事件, 解决多状态异步操作的响应问题.
1.2 API 全解
发布订阅模式, 是需要一个哈希表来存储监听事件和对应的回调函数的, 在 events 模块中, 这个哈希表 形如:(多个回调函数存储为数组, 如果没有回调函数, 不会存在对应的键值)
{
事件 A: [回调函数 1, 回调函数 2, 回调函数 3],
事件 B: 回调函数 1
}
所有 API 就是围绕这个哈希表进行增删改查操作
emitter.addListener(eventName, listener): 在哈希表中, 对应事件中增加一个回调函数
emitter.on(eventName, listener): 同 1, 别名
emitter.once(eventName, listener): 同 1, 单次监听器
emitter.prependListener(eventName, listener): 同 1, 添加在监听器数组开头
emitter.prependOnceListener(eventName, listener): 同 1, 添加在监听器数组开头 && 单次监听器
emitter.removeListener(eventName, listener): 移除指定的事件中的某个监听器
emitter.off(eventName, listener): 同上, 别名
emitter.removeAllListeners([eventName]): 移除全部监听器或者指定的事件的监听器
emitter.emit(eventName[, ...args]): 按照监听器注册的顺序, 同步地调用对应事件的监听器, 并提供传入的参数
emitter.eventNames(): 获得哈希表中所有的键值(包括 Symbol)
emitter.listenerCount(eventName): 获得哈希表中对应键值的监听器数量
emitter.listeners(eventName): 获得对应键的监听器数组的副本
emitter.rawListeners(eventName): 同上, 只不过不会对 once 处理过后的监听器还原(新增于 Node 9.4.0)
emitter.setMaxListeners(n): 设置当前实例监听器最大限制数的值
emitter.getMaxListeners(): 返回当前实例监听器最大限制数的值
EventEmitter.defaultMaxListeners: 它是每个实例的监听器最大限制数的默认值, 修改它会影响所有实例
2. 源码分析(Node.JS V10.15.1)
此部分不会从头到尾的阅读源码, 只是贴出源码中一些有趣的点! 源码阅读会放在文末.
2.1 初始化方式
- function EventEmitter() {
- // 调用 EventEmitter 类的静态方法 init 初始化
- // 我觉得这样的初始化方式包装了代码的可读性, 也提供了一个改写的方式
- EventEmitter.init.call(this)
- }
- // export first
- module.exports = EventEmitter
- // 哈希表, 保存一个 EventEmitter 实例中所有的注册事件和对应的处理函数
- EventEmitter.prototype._events = undefined
- // 计数器, 代表当前实例中注册事件的个数
- EventEmitter.prototype._eventsCount = 0
- // 监听器最大限制数量的值
- EventEmitter.prototype._maxListeners = undefined
- // EventEmitter 类的初始化静态方法
- 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
- }
为什么使用 Object.create(null)而不是直接赋值{}
Object.create(null)相对于 {} 存在性能优势(由于 Node 版本的不同, 这里的性能优势也不能说是绝对的)
Object.craete(null) 更加干净, 对它的操作不会让对象受原型链影响
- console.log({})
- // 输出
- {
- __proto__:
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
- }
- console.log(Object.create(null))
- // 输出
- {
- }
2.2 在一个事件监听器中监听同一个事件会死循环吗?
这样的代码会死循环吗?
- emitter.on('lock', function lock() {
- emitter.on('lock', lock)
- })
答案是不会, 从简化的源码中分析:
- EventEmitter.prototype.emit = function emit(type, ...args) {
- const events = this._events;
- const handler = events[type];
- // 如果仅有一个回调函数
- if (typeof handler === 'function') {
- Reflect.apply(handler, this, args)
- }
- // 如果是一个数组
- else {
- const len = handler.length
- const listeners = arrayClone(handler, len)
- for (var i = 0; i <len; ++i)
- Reflect.apply(listeners[i], this, args)
- }
- }
- // 复制数组嗷
- function arrayClone(arr, n) {
- var copy = new Array(n);
- for (var i = 0; i < n; ++i)
- copy[i] = arr[i];
- return copy;
- }
假设 lock 事件中的回调函数为 [A, B, C], 那么如果不做处理, 在执行过程中会变成 [A, B, C, Lock, Lock, Lock, ....] 导致死循环, 那么在循环之前, 先复制一份当前 的监听器数组, 那么该数组的长度就固定下来了, 也就避免了死循环.
2.3 Reflect 的使用
ES6 推出 Reflect 之后, 也基本没用过, 而在 Events 源码中有两处使用
Reflect.apply: 对一个函数进行调用操作, 同时可以传入一个数组作为调用参数. 和 Function.prototype.apply()功能类似. 在源码中用于执行监听器
Reflect.ownKeys: 返回一个包含所有自身属性 (不包含继承属性) 的数组. 在源码中用于获取哈希表中所有的事件
参考阮一峰 ES6 入门中: 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty), 放到 Reflect 对象上. 现阶段, 某些方法同时在 Object 和 Reflect 对象上部署, 未来的新方法将只部署在 Reflect 对象上. 也就是说, 从 Reflect 对象上可以拿到语言内部的方法.
- // 返回已注册监听器的事件名数组
- EventEmitter.prototype.eventNames = function eventNames() {
- // 等价于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
- return this._eventsCount> 0 ? Reflect.ownKeys(this._events) : [];
- };
这样使得代码更加易读! 另外补上一个绕口令一般的存在
- function test(a, b) {
- return a + b
- }
- Function.prototype.apply.call(test, undefined, [1, 3]) // 4
- Function.prototype.call.call(test, undefined, 1, 3) // 4
- Function.prototype.call.apply(test, [undefined, 1, 3]); // 4
- Function.prototype.apply.apply(test, [undefined, [1, 3]]); // 4
2.4 单次监听器是如何实现的?
源码
- // 添加单次监听器
- EventEmitter.prototype.once = function once(type, listener) {
- // 参数检查
- checkListener(listener);
- // on 是 addEventListener 的别名
- this.on(type, _onceWrap(this, type, listener));
- return this;
- };
从这里可以得出结论: 对监听函数包装了一层!
- // 参数分别代表: 当前 events 实例, 事件名称, 监听函数
- function _onceWrap(target, type, listener) {
- // 拓展 this
- // {
- // fired: 标识位, 是否应当移除此监听器
- // wrapFn: 包装后的函数, 用于移除监听器
- // }
- var state = { fired: false, wrapFn: undefined, target, type, listener };
- var wrapped = onceWrapper.bind(state);
- // 真正的监听器
- wrapped.listener = listener;
- state.wrapFn = wrapped;
- // 返回包装后的函数
- return wrapped;
- }
- function onceWrapper(...args) {
- if (!this.fired) {
- // 监听器会先被移除, 然后再调用
- this.target.removeListener(this.type, this.wrapFn);
- this.fired = true;
- Reflect.apply(this.listener, this.target, args);
- }
- }
2.5 效率更高的从数组中去除一个元素
在 EventEmitter#removeListener 这个 API 的实现里, 需要从存储的监听器数组中去除一个元素, 首先想到的就是 Array#splice 这个 API, 不过这个 API 提供的功能过于多了, 它支持去除自定义数量的元素, 还支持向数组中添加自定义的元素, 所以, 源码中选择自己实现一个最小可用的
因此你会在源码中看到
- var splceOnce
- EventEmitter.prototype.removeListener = function removeListener(type, listener) {
- var events = this._events
- var list = events[type]
- // As of V8 6.6, depending on the size of the array, this is anywhere
- // between 1.5-10x faster than the two-arg version of Array#splice()
- // function spliceOne(list, index) {
- // for (; index + 1 < list.length; index++)
- // list[index] = list[index + 1];
- // list.pop();
- // }
- if (spliceOne === undefined)
- spliceOne = require('internal/util').spliceOne;
- spliceOne(list, position);
- }
spliceOne, 很好理解
- function spliceOne(list, index) {
- for (; index + 1 < list.length; index++)
- list[index] = list[index + 1];
- list.pop();
- }
2.6 正确修改当前实例监听器限制
修改 EventEmitter.defaultMaxListeners, 会影响所有 EventEmitter 实例, 包括之前创建的
调用 emitter.setMaxListeners(n), 只会影响当前实例的监听器限制
限制不是强制的, 有助于避免内存泄漏, 超过限制只会输出警示信息.
相关源码
- var defaultMaxListeners = 10
- Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
- enumerable: true,
- get: function() {
- return defaultMaxListeners;
- },
- set: function(arg) {
- if (typeof arg !== 'number' || arg < 0 || Number.isNaN(arg)) {
- const errors = lazyErrors();
- throw new errors.ERR_OUT_OF_RANGE('defaultMaxListeners',
- 'a non-negative number',
- arg);
- }
- defaultMaxListeners = arg;
- }
- });
另一部分
// 为指定的 EventEmitter 实例修改限制 EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) { const errors = lazyErrors(); throw new errors.ERR_OUT_OF_RANGE('n', 'a non-negative number', n); } this._maxListeners = n; return this; }; function $getMaxListeners(that) { // 当前实例监听器限制的默认值为静态属性 defaultMaxListeners 的值 // 这也是为什么修改它会影响所有的原因 if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); };
3. 源码注释版地址~
源码注释版地址
参考
通过源码解析 Node.JS 中 events 模块里的优化小细节 https://cnodejs.org/topic/571e0c445a26c4a841ecbcf1
来源: https://juejin.im/post/5c55512af265da2deb6a7dc8