Node.js EventEmitter類源碼淺析

寫在最前

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

歡迎關注個人博客,不按期更新中——node

EventEmitter

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

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

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至關於一箇中介,負責記錄都訂閱了哪些事件而且觸發後的回調是什麼,當事件被觸發,就將回調一一執行。github

發佈訂閱模式

從源碼中看下EventEmitter類的是如何實現發佈訂閱的。
首先咱們梳理一下實現這個模式須要的步驟:chrome

  1. 初始化空對象用來存儲監聽事件與對應的回調函數
  2. 添加監聽事件,註冊回調函數
  3. 觸發事件,找出對應回調函數隊列,一一執行
  4. 刪除監聽事件

初始化空對象

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

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中。app

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

在觸發的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/...

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

相關文章
相關標籤/搜索