这一节内容超级简单, 纯 JS, 就当给自己放个假了, V8 引擎和 node 的 C++ 代码看得有点脑阔疼.
学过 DOM 的应该都知道一个 API, 叫 addeventlistener, 即事件绑定. 这个东西贯穿了整个 JS 的学习过程, 无论是刚开始的自己获取 DOM 手动绑, 还是后期 vue 的直接 @click, 所有的交互都离不开这个东西.
同样, 在 node 中, 事件绑定也贯穿了整个框架. 基本上大多数的内置模块以 events 为原型, 下面的代码随处可见:
EventEmitter.call(this);
不同的是, 页面上 DOM 的事件绑定是由浏览器来实现, 触发也是一些操作'间接'触发, 并不需要去主动 emit 对应事件, 并且有冒泡和捕获这两特殊的性质.
但是在 node 中, 不存在 dom, 绑定的目标是一个对象(dom 本质上也是对象), 在内部 node 自己用纯 JS 实现了一个事件绑定与事件触发类.
本文相关源码来源于 https://github.com/nodejs/node/blob/master/lib/events.js.
首先看一下构造函数:
- function EventEmitter() {
- EventEmitter.init.call(this);
- }
这里会调用一个 init 方法, this 指向调用对象, 初始化方法也很简单:
- 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;
- };
涉及的三个属性分别是:
1,_events => 一个挂载属性, 空对象, 负责收集所有类型的事件
2,_eventsCount => 记录目前绑定事件类型的数量
3,_maxListeners => 同类型事件 listener 数量限制
事件相关的主要操作有 3 个, 依次来看.
绑定事件 / on
虽然一般用的 AP 都是 event.on, 但是其实用 addListener 是一样的:
- EventEmitter.prototype.on = EventEmitter.prototype.addListener;
- EventEmitter.prototype.addListener = function addListener(type, listener) {
- return _addListener(this, type, listener, false);
- };
这个 addListener 跟 DOM 的 addEventListener 稍微有点不一样, 前两个参数一致, 分别代表类型, 回调函数.
但是最后一个参数, 这里代表的是否优先插入该事件, 有一个方法就是做这个的:
- EventEmitter.prototype.prependListener =
- function prependListener(type, listener) {
- return _addListener(this, type, listener, true);
- };
最终都指向这个_addListener, 分步解释:
- /**
- * 事件绑定方法
- * @param {Object} target 目标对象
- * @param {String} type 事件名称
- * @param {Function} listener 回调函数
- * @param {Boolean} prepend 是否插入
- */
- function _addListener(target, type, listener, prepend) {
- // 指定事件类型的回调函数数量
- var m;
- // 事件属性对象
- var events;
- // 对应类型的回调函数
- var existing;
- if (typeof listener !== 'function') {
- const errors = lazyErrors();
- throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
- }
- // 尝试获取对应类型的事件
- events = target._events;
- // 未找到对应的事件相关属性
- if (events === undefined) {
- events = target._events = Object.create(null);
- target._eventsCount = 0;
- }
- // 当存在对象的事件属性对象时
- else {}
- // more...
- return target;
- }
这里首先会尝试获取指定对象的_events 属性, 即构造函数中初始化的挂载对象属性.
由于无论是任意构造函数中调用 EventEmitter.call(this)或者 new EventEmitter()都会在生成对象上挂载一个_events 对象, 所以这个判断暂时找不到反例.
当不存在就手动初始化一个, 并添加一个记数属性重置为 0.
当存在时, 处理代码如下:
- events = target._events;
- if (events === undefined) {
- // ...
- } else {
- // To avoid recursion in the case that type === "newListener"! Before
- // adding it to the listeners, first emit "newListener".
- if (events.newListener !== undefined) {
- target.emit('newListener', type,
- listener.listener ? listener.listener : listener);
- // Re-assign `events` because a newListener handler could have caused the
- // this._events to be assigned to a new object
- events = target._events;
- }
- // 尝试获取对应类型的回调函数集合
- existing = events[type];
- }
这个地方的注释主要讲的是, 当绑定了 type 为 newListener 的事件时, 每次都会触发一次这个事件, 如果再次绑定该事件会出现递归问题. 所以要判断是否存在 newListener 事件类型, 如果有就先触发一次 newListener 事件.
先不管这个, 最后会尝试获取指定类型的事件 listener 容器, 下面就是对 existing 的处理.
- // 首次添加该类型事件时
- if (existing === undefined) {
- // 直接把函数赋值给对应类型的 key
- existing = events[type] = listener;
- // 记数 + 1
- ++target._eventsCount;
- } else {
- // 1. 已有对应类型 但是只有一个
- if (typeof existing === 'function') {
- // 转换数组 根据 prepend 参数安排顺序
- existing = events[type] =
- prepend ? [listener, existing] : [existing, listener];
- // If we've already got an array, just append.
- }
- // 2. 已有多个 判断是否有优先的 flag 进行前插或后插
- else if (prepend) {
- existing.unshift(listener);
- } else {
- existing.push(listener);
- }
- // Check for listener leak
- // ...
- }
这里的处理就能很清楚的看到 events 模块对于事件绑定的处理,_events 相当于一个总对象, 属性的 key 就是对应的事件类型 type, 而 key 对应的 value 就是对应的 listener. 只有一个时, 就直接用该 listener 做值. 重复绑定同类型的事件, 这时值会转换为数组保存所有的 listener. 这里 prepend 就是之前的最后一个参数, 允许函数插入到队列的前面, 优先触发.
最后还有一个绑定事件的数量判断:
- // 获取_maxListeners 参数 同类型事件 listener 最大绑定数量
- m = $getMaxListeners(target);
- // 如果超出就发出可能有内存泄漏的警告
- if (m> 0 && existing.length> m && !existing.warned) {
- existing.warned = true;
- // 因为是 warning 所以不会有 error code 可以不理这个东西
- // eslint-disable-next-line no-restricted-syntax
- const w = new Error('Possible EventEmitter memory leak detected.' +
- `${existing.length} ${String(type)} listeners ` +
- 'added. Use emitter.setMaxListeners() to' +
- 'increase limit');
- w.name = 'MaxListenersExceededWarning';
- w.emitter = target;
- w.type = type;
- w.count = existing.length;
- process.emitWarning(w);
- }
看看就好, 程序员不用管 warning, 哈哈.
一次绑定事件 / once
有些时候希望事件只触发一次, 原生的 API 目前不存在该功能, 当初 jquery 也是封装了一个 once 方法, 对应的这个 events 模块也有.
- EventEmitter.prototype.once = function once(type, listener) {
- if (typeof listener !== 'function') {
- const errors = lazyErrors();
- throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
- }
- this.on(type, _onceWrap(this, type, listener));
- return this;
- };
除去那个判断, 其实绑定的方法还是同一个, 只是对应的 listener 变成了一个包装函数, 来看看.
- function _onceWrap(target, type, listener) {
- // this 绑定对象
- var state = { fired: false, wrapFn: undefined, target, type, listener };
- var wrapped = onceWrapper.bind(state);
- // 原生的 listener 挂载到这个包装函数上
- wrapped.listener = listener;
- // 处理完后更新 state 属性
- state.wrapFn = wrapped;
- // 返回的是一个包装的函数
- return wrapped;
- }
- function onceWrapper(...args) {
- // 这里所有的 this 指向上面的 state 对象
- // args 来源于触发时候给的参数
- if (!this.fired) {
- // 解绑该包装后的 listener
- this.target.removeListener(this.type, this.wrapFn);
- this.fired = true;
- // 触发 listener
- Reflect.apply(this.listener, this.target, args);
- }
- }
思路其实跟 jquery 的源码差不多, 也是包装 listener, 当触发一次事件时, 先解绑这个 listener 再触发事件.
需要注意的是, 这里存在两个 listener, 一个是原生的, 一个是包装后的. 绑定的是包装的, 所以解绑的第二个参数也要是包装的. 其中原生的作为 listener 属性挂载到包装后的函数上, 实际上触发包装 listener 后内部会隐式调用原生 listener.
事件触发 / emit
看完绑定, 来看触发.
- EventEmitter.prototype.emit = function emit(type, ...args) {
- let doError = (type === 'error');
- const events = this._events;
- // 判断是否触发的 error 类型事件
- if (events !== undefined)
- doError = (doError && events.error === undefined);
- else if (!doError)
- return false;
- // If there is no 'error' event listener then throw.
- if (doError) {
- // 错误处理 不看
- }
- // 跟之前的 existing 一个东西
- const handler = events[type];
- if (handler === undefined)
- return false;
- // 如果只有一个 直接调用
- if (typeof handler === 'function') {
- Reflect.apply(handler, this, args);
- } else {
- // 多个 listener 依次触发
- const len = handler.length;
- const listeners = arrayClone(handler, len);
- for (var i = 0; i <len; ++i)
- Reflect.apply(listeners[i], this, args);
- }
- return true;
- };
太简单了, 懒得解释.
事件解绑 / removeListener
同样分几步来看解绑的过程, 首先是参数声明:
- // Emits a 'removeListener' event if and only if the listener was removed.
- EventEmitter.prototype.removeListener = function removeListener(type, listener) {
- // list => listener 容器
- // events => 事件根对象
- // position => 记录删除 listener 位置
- // i => 迭代参数
- // originalListener => 原生 listener 参考上面的 once
- var list, events, position, i, originalListener;
- if (typeof listener !== 'function') {
- const errors = lazyErrors();
- throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
- }
- events = this._events;
- if (events === undefined)
- return this;
- list = events[type];
- if (list === undefined)
- return this;
- // ...
- }
比较简单, 每个参数的用处都很明显, 错误判断后, 下面有两种不同的情况.
当对应 type 的 listener 只有一个时:
- EventEmitter.prototype.removeListener = function removeListener(type, listener) {
- // list => listener 容器
- // events => 事件根对象
- // position => 记录删除 listener 位置
- // i => 迭代参数
- // originalListener => 原生 listener 参考上面的 once
- var list, events, position, i, originalListener;
- // ...
- // listener 只有一个的情况
- if (list === listener || list.listener === listener) {
- // 如果一个绑定事件都没了 直接重置_events 对象
- if (--this._eventsCount === 0)
- this._events = Object.create(null);
- else {
- // 删除对应的事件类型
- delete events[type];
- // 尝试触发一次 removeListener 事件
- if (events.removeListener)
- this.emit('removeListener', type, list.listener || listener);
- }
- } else if (typeof list !== 'function') {
- // ...
- }
- return this;
- };
这里还分了两种情况, 如果_eventsCount 为 0, 即所有的 type 都被清完, 会重置_events 对象.
理论上来说, 按照 else 分支的逻辑, 当 listener 剩一个的时候都是直接 delete 对应的 key, 最后剩下的还是一个空对象, 那这里的重重置似乎变得没有意义了.
我猜测估计是为了 V8 层面的优化, 因为对象的属性在破坏性变动 (如重复绑定同 type 事件导致函数变成函数数组) 的时候, 所需的内存会进行扩充, 这个过程是不可逆的, 就算最后只剩一个空壳对象, 其实际占用也是相当大的. 所以为了省空间, 这里进行重置, 用很小的空间初始化_events 对象, 原来的空间被回收.
当对应 type 的 listener 为多个时, 就要遍历了.
- if (list === listener || list.listener === listener) {
- // ...
- } else if (typeof list !== 'function') {
- position = -1;
- // 倒序遍历
- for (i = list.length - 1; i>= 0; i--) {
- if (list[i] === listener || list[i].listener === listener) {
- // once 绑定的事件有 listener 属性
- originalListener = list[i].listener;
- // 记录位置
- position = i;
- break;
- }
- }
- if (position < 0)
- return this;
- // 在第一个位置时
- if (position === 0)
- list.shift();
- else {
- // 删除数组对应索引的值
- if (spliceOne === undefined)
- spliceOne = require('internal/util').spliceOne;
- spliceOne(list, position);
- }
- // 如果数组里只有一个值 转换为单个值
- // 有点像 HashMap 的链表 - 红黑树转换......
- if (list.length === 1)
- events[type] = list[0];
- // 尝试触发 removeListener
- if (events.removeListener !== undefined)
- this.emit('removeListener', type, originalListener || listener);
- }
太简单了, 自己看吧.
其他还有诸如 removeAllListeners,_listeners,eventNames 等 API, 有兴趣的可以自行去看.
来源: https://www.cnblogs.com/QH-Jimmy/p/9438769.html