你有沒有想過,為什麼瀏覽器的 div 上可以繫結多個 onclick
事件,點選一下 div 可以觸發全部的事件,jquery 的 .on()
,.off()
,one()
又是如何實現的?Node.js 事件驅動的原理是怎樣的?
實際上這一切都是 EventEmitter 在背後做支援,它是 JavaScript 經典的事件驅動實現,現在我們來看下 Node.js 中是如何實現的。
本文所說的監聽事件在實現上都為函式,讀者可以認為兩者相等以方便閱讀。
因為原來的 Node 程式碼量比較多,為了方便演示,作者把本文的原始碼示例中涉及資料驗證,錯誤處理的部分刪除,保留了主要內容。
準備:
- 原始碼一份:github.com/nodejs/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
_events
不存在時,使用 Object.create(null)
來初始化,並把 _eventsCount
設 0。
劃重點 —— Object.create(null)
可以建立一個沒有原型的物件。
為什麼要用這種方法建立物件呢?開發者這麼做的目的其實還是出於效能上的考慮,因為 EventEmitter 在 Node.js 中應用廣泛,為節省伺服器記憶體和執行速度上不必要的開銷,肯定能省則省唄。
2. 新增事件繫結 addListener
首先判斷 target
的 _events
是否存在,如果不存在則還是用 Object.create(null)
建立。
如果存在,觸發 newListener
型別的事件。然後通過 event[type]
找到已經註冊 type 型別的監聽事件/監聽事件陣列,並存到 existing
中。
如果該事件值為 undefined
,則把直接把要註冊的監聽事件 listener
賦給不存在的事件。否則,更新事件陣列(單一的 listener
要轉為陣列)。
注意 prepend 的使用,可以靈活地把 listener
新增到監聽函式陣列頭部或尾部。
3. 事件新增到陣列頭部 prependListener
和 addListener 類似,但是prepend
為 true
。
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 的同學來說是必須掌握它的。
歡迎溝通評論和交流!!!如果這篇文章幫助到了你,麻煩給個小心心哦❤️❤️❤️