NodeJS Events模組原始碼學習

hsabalaaaC發表於2019-02-02

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 模組裡的優化小細節

相關文章