[原] 探索 EventEmitter 在 Node.js 中的实现
你有沒有想過,為什么瀏覽器的 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 的同學來說是必須掌握它的。
歡迎溝通評論和交流!!!如果這篇文章幫助到了你,麻煩給個小心心哦??????
總結
以上是生活随笔為你收集整理的[原] 探索 EventEmitter 在 Node.js 中的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Flutter之后,我们的CPU占用
- 下一篇: MyEclipse打不开 报xxxxx