NodeJS Events模块源码学习

栏目: Node.js · 发布时间: 5年前

内容简介:回调函数模式让为了解决这个问题,

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);
};
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Bulletproof Web Design

Bulletproof Web Design

Dan Cederholm / New Riders Press / 28 July, 2005 / $39.99

No matter how visually appealing or packed with content a Web site is, it isn't succeeding if it's not reaching the widest possible audience. Designers who get this guide can be assured their Web site......一起来看看 《Bulletproof Web Design》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具