内容简介:回调函数模式让为了解决这个问题,
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); }; 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Zepto源码学习Event模块
- NodeJS Cluster模块源码学习
- 试读angular源码第四章:angular模块及JIT编译模块
- 对公司内部某个模块某个源码审计
- 比特币源码分析:txdb 模块(三)
- 比特币源码分析:txdb 模块(二)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
屏幕上的聪明决策
Shlomo Benartzi、Jonah Lehrer / 石磊 / 北京联合出版公司 / 2017-3 / 56.90
为什么在手机上购物的人,常常高估商品的价值? 为什么利用网络订餐,人们更容易选择热量高的食物? 为什么网站上明明提供了所有选项,人们却还是选不到最佳的方案? 屏幕正在改变我们的思考方式,让我们变得更冲动,更容易根据直觉做出反应,进而做出错误的决策。在《屏幕上的聪明决策》一书中,什洛莫·贝纳茨教授通过引人入胜的实验及案例,揭示了究竟是什么影响了我们在屏幕上的决策。 ......一起来看看 《屏幕上的聪明决策》 这本书的介绍吧!