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

你有沒有想過,爲何瀏覽器的 div 上能夠綁定多個 onclick 事件,點擊一下 div 能夠觸發所有的事件,jquery 的 .on().off()one() 又是如何實現的?Node.js 事件驅動的原理是怎樣的?node

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

本文所說的監聽事件在實現上都爲函數,讀者能夠認爲二者相等以方便閱讀。git

由於原來的 Node 代碼量比較多,爲了方便演示,做者把本文的源代碼示例中涉及數據驗證,錯誤處理的部分刪除,保留了主要內容。github

準備:

概覽 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

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

劃重點 —— Object.create(null) 能夠建立一個沒有原型的對象服務器

爲何要用這種方法建立對象呢?開發者這麼作的目的其實仍是出於性能上的考慮,由於 EventEmitter 在 Node.js 中應用普遍,爲節省服務器內存和執行速度上沒必要要的開銷,確定能省則省唄。閉包

2. 添加事件綁定 addListener

首先判斷 target_events 是否存在,若是不存在則仍是用 Object.create(null) 建立。app

若是存在,觸發 newListener 類型的事件。而後經過 event[type] 找到已經註冊 type 類型的監聽事件/監聽事件數組,並存到 existing 中。

若是該事件值爲 undefined,則把直接把要註冊的監聽事件 listener 賦給不存在的事件。不然,更新事件數組(單一的 listener 要轉爲數組)。

注意 prepend 的使用,能夠靈活地把 listener 添加到監聽函數數組頭部或尾部。

3. 事件添加到數組頭部 prependListener

和 addListener 相似,可是 prependtrue

2. 觸發事件 emit

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

若 handler 是一個函數,使用 Reflect 調用函數。若是是數組的話則遍歷數組並調用,而後返回 true。

3 移除事件綁定 removeListener

type 取出要刪除的監聽函數列表 list = event[type],當 list 等於要刪除的監聽函數時,_eventsCount 減一後若是爲 0,直接初始化 _events,不然只刪除當前類型的監聽函數。

接着往下看,若 typeof list !== 'function'list 爲數組時,先肯定要刪除監聽事件的位置 position,而後刪掉對應的函數。

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

由於這個兩參數的方法要比內置的 splice 可能快上 1.5 - 10 倍!我專門查看了下提交記錄,這個版本的方法通過幾個開發者改動過最終成爲如今這個樣子。不得不佩服各路大神對開源的貢獻!至於 splice 爲何慢,我沒能查到緣由,也許須要去看 v8 源碼。

4 事件只能執行一次 once

這個方法的實現有點 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 的同窗來講是必須掌握它的。

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

相關文章
相關標籤/搜索