Node.js EventEmitter類源碼淺析

寫在最前

本次嘗試淺析Node.js中的EventEmitter模塊的事件機制,分析在Node.js中實現發佈訂閱模式的一些細節。完整Node.js源碼點這裏。

歡迎關注我的博客,不定期更新中——

EventEmitter

大多數 Node.js 核心 API 都採用慣用的異步事件驅動架構,其中某些類型的對象(觸發器)會週期性地觸發命名事件來調用函數對象(監聽器)。例如,net.Server 對象會在每次有新連接時觸發事件;fs.ReadStream 會在文件被打開時觸發事件;流對象 會在數據可讀時觸發事件。所有能觸發事件的對象都是 EventEmitter 類的實例。

Node.js中對EventEmitter類的實例的運用可以說是貫穿整個Node.js,相信這一點大家已經是很熟悉的了。其中所運用到的發佈訂閱模式,則是很經典的管理消息分發的一種方式。在這種模式中,發佈消息的一方不需要知道這個消息會給誰,而訂閱的一方也無需知道消息的來源。使用方式一般如下:

const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('觸發了一個事件A!');
});
myEmitter.emit('event');
//觸發了一個事件A!

當我們訂閱了’event’事件後,可以在任何地方通過emit('event')來執行事件回調,EventEmitter相當於一箇中介,負責記錄都訂閱了哪些事件並且觸發後的回調是什麼,當事件被觸發,就將回調一一執行。

發佈訂閱模式

從源碼中看下EventEmitter類的是如何實現發佈訂閱的。
首先我們梳理一下實現這個模式需要的步驟:
1. 初始化空對象用來存儲監聽事件與對應的回調函數
2. 添加監聽事件,註冊回調函數
3. 觸發事件,找出對應回調函數隊列,一一執行
4. 刪除監聽事件

初始化空對象

在生成空對象的方式中,一般容易想到的是直接進行賦值空對象即var a = {};,Node.js中採用的方式爲var a = Object.create(null),使用這種方式理論上是應該對對象的屬性存取的操作更快,出於好奇作者對這兩種方式做了個粗略的對比:

var a = {} 
a.test = 1
var b = Object.create(null)
b.test = 1
console.time('{}')
for(var i = 0; i < 1000; i++) {
    console.log(a.test)
}
console.timeEnd('{}')
console.time('create')
for(var i = 0; i < 1000; i++) {
    console.log(b.test)
}
console.timeEnd('create')

image
image

打印結果顯示出來貌似直接用空對象賦值與通過Object.create的方式並沒有很大的性能差異,並且還沒有誰一定佔了上風,就目前該空對象用來存儲註冊的監聽事件與回調來看,如果直接用{}來初始化this._events性能方面影響也許不大。不過這一點只是個人觀點,暫時還並不能領會Node裏面如此運用的深意。

添加監聽事件,註冊回調函數

EventEmitter.prototype.addListener = function addListener(type, listener) {
  return _addListener(this, type, listener, false);
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

添加監聽者的方法爲addListener,同時on是其別名。

if (!existing) {
    // Optimize the case of one listener. Don't need the extra array object.
    existing = events[type] = listener;
    ++target._eventsCount;
} else {
    if (typeof existing === 'function') {
      // Adding the second element, need to change to array.
      existing = events[type] =
        prepend ? [listener, existing] : [existing, listener];
    } else {
      // If we've already got an array, just append.
      if (prepend) {
        existing.unshift(listener);
      } else {
        existing.push(listener);
      }
}
  ...
}

如果之前不存在監聽事件,則會進入第一個判斷內,其中type爲事件類型,listener爲觸發的事件回調。如果之前註冊過事件,那麼回調函數會添加到回調隊列的頭或尾。看如下打印結果:

myEmitter.on('event', () => {
  console.log('觸發了一個事件A!');
});
myEmitter.on('event', () => {
    console.log('觸發了一個事件B!');
});
myEmitter.on('talk', () => {
    console.log('觸發了一個事件CS!');
    // myEmitter.emit('talk');
});
console.log(myEmitter._events)
//{ event: [ [Function], [Function] ], talk: [Function] }

myEmitter實例的_events方法就是我們存儲事件與回調的對象,可以看到當我們依次註冊事件後,回調會被推到 _events對應key的value中。

觸發事件,找出對應回調函數隊列,一一執行

在觸發的emit函數中,會根據觸發時傳入參數的多少執行不同的函數:(參數不同直接執行不同的函數,這個操作應該會讓性能更好,不過作者沒有測試這點)

switch (len) {
    // fast cases
    case 1:
      emitNone(handler, isFn, this);
      break;
    case 2:
      emitOne(handler, isFn, this, arguments[1]);
      break;
    case 3:
      emitTwo(handler, isFn, this, arguments[1], arguments[2]);
      break;
    case 4:
      emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]);
      break;
    // slower
    default:
      args = new Array(len - 1);
      for (i = 1; i < len; i++)
        args[i - 1] = arguments[i];
      emitMany(handler, isFn, this, args);
  }

以emitMany爲例看下內部觸發實現:

var isFn = typeof handler === 'function';
function emitMany(handler, isFn, self, args) {
  if (isFn)
  //handler類型爲函數,即對這個事件只註冊了一個監聽函數
    handler.apply(self, args);
  else { 
  //當對同一事件註冊了多個監聽函數的時候,handler類型爲數組
    var len = handler.length;
    var listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i)
      listeners[i].apply(self, args);
  }
}
function arrayClone(arr, n) {
  var copy = new Array(n);
  for (var i = 0; i < n; ++i)
    copy[i] = arr[i];
  return copy;
}

源碼中實現了arrayClone方法,來複制一份同樣的監聽函數,再去依次執行副本。個人對這個做法的理解是,當觸發當前類型事件後,就鎖定需要執行的回調函數隊列,否則當觸發回調過程中,再去推入新的回調函數,或者刪除已有回調函數,容易造成不可預知的問題。

刪除監聽事件

如果回調事件只有一個那麼直接刪除即可,如果是數組就像之前看到的那樣註冊了多組對同樣事件的監聽,就要涉及從數組中刪除項的實現。在這裏Node自己實現了一個spliceOne函數來代替原生的splice,並且說明其方式比splice快1.5倍。下面是作者進行的簡易粗略,不嚴謹的運行時間比較:
image
上面做了一個很粗略的運算時間比較,同樣是對長度爲1000的數組第100項進行刪除操作,並且代碼運行在chrome瀏覽器下(版本號61.0.3163.100)node源碼中自己實現的方法確實比原生的splice快了一些,不過結果只是一個參考畢竟這個對比很粗略,有興趣的童鞋可以寫一組benchmark來進行對比。

參考資料

最後

源碼的邊界情況比較多。在這裏只做一個相對簡單的流程淺析,哪裏說明有誤歡迎指正~
PS:相關實例源碼:https://github.com/Aaaaaaaty/Blog/blob/master/node/event.js

慣例po作者的博客,不定時更新中——
有問題歡迎在issues下交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章