[原] 探索 EventEmitter 在 Node.js 中的實現

子非發表於2019-02-22

[原] 探索 EventEmitter 在 Node.js 中的實現

你有沒有想過,為什麼瀏覽器的 div 上可以繫結多個 onclick 事件,點選一下 div 可以觸發全部的事件,jquery 的 .on().off()one() 又是如何實現的?Node.js 事件驅動的原理是怎樣的?

實際上這一切都是 EventEmitter 在背後做支援,它是 JavaScript 經典的事件驅動實現,現在我們來看下 Node.js 中是如何實現的。

本文所說的監聽事件在實現上都為函式,讀者可以認為兩者相等以方便閱讀。

因為原來的 Node 程式碼量比較多,為了方便演示,作者把本文的原始碼示例中涉及資料驗證,錯誤處理的部分刪除,保留了主要內容。

準備:

概覽 EventEmitter

內部屬性:

  • _events:用來儲存監聽事件,可以是一個事件或事件陣列。
  • _eventsCount:記錄已註冊的監聽事件個數。

主要方法:

  • emitter.addListener/on(eventName, listener) 新增型別為 eventName 的監聽事件到事件陣列尾部
  • emitter.prependListener(eventName, listener) 新增型別為 eventName 的監聽事件到事件陣列頭部
  • emitter.emit(eventName[, ...args]) 觸發型別為 eventName 的監聽事件
  • emitter.removeListener/off(eventName, listener) 移除型別為 eventName 的監聽事件
  • emitter.once(eventName, listener) 新增型別為 eventName 的監聽事件,以後只能執行一次並刪除
  • emitter.removeAllListeners([eventName]) 移除全部型別為 eventName 的監聽事件

正文

1. 初始化 init

[原] 探索 EventEmitter 在 Node.js 中的實現

_events 不存在時,使用 Object.create(null) 來初始化,並把 _eventsCount 設 0。

劃重點 —— Object.create(null) 可以建立一個沒有原型的物件

為什麼要用這種方法建立物件呢?開發者這麼做的目的其實還是出於效能上的考慮,因為 EventEmitter 在 Node.js 中應用廣泛,為節省伺服器記憶體和執行速度上不必要的開銷,肯定能省則省唄。

2. 新增事件繫結 addListener

[原] 探索 EventEmitter 在 Node.js 中的實現

首先判斷 target_events 是否存在,如果不存在則還是用 Object.create(null) 建立。

如果存在,觸發 newListener 型別的事件。然後通過 event[type] 找到已經註冊 type 型別的監聽事件/監聽事件陣列,並存到 existing 中。

如果該事件值為 undefined,則把直接把要註冊的監聽事件 listener 賦給不存在的事件。否則,更新事件陣列(單一的 listener 要轉為陣列)。

注意 prepend 的使用,可以靈活地把 listener 新增到監聽函式陣列頭部或尾部。

3. 事件新增到陣列頭部 prependListener

[原] 探索 EventEmitter 在 Node.js 中的實現
和 addListener 類似,但是 prependtrue

2. 觸發事件 emit

[原] 探索 EventEmitter 在 Node.js 中的實現

若 handler 不存在,直接返回 false。

若 handler 是一個函式,使用 Reflect 呼叫函式。如果是陣列的話則遍歷陣列並呼叫,然後返回 true。

3 移除事件繫結 removeListener

[原] 探索 EventEmitter 在 Node.js 中的實現

type 取出要刪除的監聽函式列表 list = event[type],當 list 等於要刪除的監聽函式時,_eventsCount 減一後如果為 0,直接初始化 _events,否則只刪除當前型別的監聽函式。

接著往下看,若 typeof list !== 'function'list 為陣列時,先確定要刪除監聽事件的位置 position,然後刪掉對應的函式。

注意:為什麼不用 list.splice(postion, 1) 而要專門寫一個 spliceOne 來刪除呢?

[原] 探索 EventEmitter 在 Node.js 中的實現

因為這個兩引數的方法要比內建的 splice 可能快上 1.5 - 10 倍!我專門檢視了下提交記錄,這個版本的方法經過幾個開發者改動過最終成為現在這個樣子。不得不佩服各路大神對開源的貢獻!至於 splice 為什麼慢,我沒能查到原因,也許需要去看 v8 原始碼。

4 事件只能執行一次 once

[原] 探索 EventEmitter 在 Node.js 中的實現

這個方法的實現有點 tricky,為了維護 fired 的狀態它用到了閉包

其它還有一些方法,我不再多寫了,基本上原理就是這樣。有興趣的同學可以自己點選前文的原始碼連結檢視。

下面是我抄 Node.js 的 EventEmitter 簡單程式碼實現:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(type, handler) {
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(handler);
  }

  off(type, handler) {
    if (!this.events[type]) {
      return;
    }
    this.events[type] = this.events[type].filter(item => item !== handler);
  }

  emit(type, ...args) {
    this.events[type].forEach((item) => {
      Reflect.apply(item, this, args);
    });
  }

  once(type, handler) {
    this.on(type, this._onceWrap(type, handler, this));
  }

  _onceWrap(type, handler, target) {
    const state = { fired: false, handler, type , target};
    const wrapFn = this._onceWrapper.bind(state);
    state.wrapFn = wrapFn;
    return wrapFn;
  }

  _onceWrapper(...args) {
    if (!this.fired) {
      this.fired = true;
      Reflect.apply(this.handler, this.target, args);
      this.target.off(this.type, this.wrapFn);
    }
  }
}
// 初始化
const ee = new EventEmitter();

// 註冊所有事件
ee.once('wakeUp', (name) => { console.log(`${name}起來啦`); });
ee.on('eat', (name) => { console.log(`${name}吃饅頭啦`) });
ee.on('eat', (name) => { console.log(`${name}喝水啦`) });
const meetingFn = (name) => { console.log(`${name}開早會啦`) };
ee.on('work', meetingFn);
ee.on('work', (name) => { console.log(`${name}碼程式碼啦`) });

ee.emit('wakeUp', '子非');
ee.emit('wakeUp', '子非');         // 第二次沒有觸發
ee.emit('eat', '子非');
ee.emit('work', '子非');
ee.off('work', meetingFn);        // 移除開會事件
ee.emit('work', '子非');           // 再次工作


輸出:
子非起來啦
子非吃饅頭啦
子非喝水啦
子非開早會啦
子非碼程式碼啦
子非碼程式碼啦
複製程式碼

總結:

讀完 Node.js 的 EventEmitter 實現,一些細節上的處理我覺得非常棒,而設計層面上,優秀的包裝和抽象思路也讓我覺得十分經典。EventEmitter 非常重要,很多大型庫像 Webpack,Socket.io 都是基於它來實現的,對於學習 Js 的同學來說是必須掌握它的。

歡迎溝通評論和交流!!!如果這篇文章幫助到了你,麻煩給個小心心哦❤️❤️❤️

相關文章