[原] 探索 EventEmitter 在 Node.js 中的实现
你有沒(méi)有想過(guò),為什么瀏覽器的 div 上可以綁定多個(gè) onclick 事件,點(diǎn)擊一下 div 可以觸發(fā)全部的事件,jquery 的 .on(),.off(),one() 又是如何實(shí)現(xiàn)的?Node.js 事件驅(qū)動(dòng)的原理是怎樣的?
實(shí)際上這一切都是 EventEmitter 在背后做支持,它是 JavaScript 經(jīng)典的事件驅(qū)動(dòng)實(shí)現(xiàn),現(xiàn)在我們來(lái)看下 Node.js 中是如何實(shí)現(xiàn)的。
本文所說(shuō)的監(jiān)聽(tīng)事件在實(shí)現(xiàn)上都為函數(shù),讀者可以認(rèn)為兩者相等以方便閱讀。
因?yàn)樵瓉?lái)的 Node 代碼量比較多,為了方便演示,作者把本文的源代碼示例中涉及數(shù)據(jù)驗(yàn)證,錯(cuò)誤處理的部分刪除,保留了主要內(nèi)容。
準(zhǔn)備:
- 源碼一份:github.com/nodejs/node…
概覽 EventEmitter
內(nèi)部屬性:
- _events:用來(lái)存儲(chǔ)監(jiān)聽(tīng)事件,可以是一個(gè)事件或事件數(shù)組。
- _eventsCount:記錄已注冊(cè)的監(jiān)聽(tīng)事件個(gè)數(shù)。
主要方法:
- emitter.addListener/on(eventName, listener) 添加類(lèi)型為 eventName 的監(jiān)聽(tīng)事件到事件數(shù)組尾部
- emitter.prependListener(eventName, listener) 添加類(lèi)型為 eventName 的監(jiān)聽(tīng)事件到事件數(shù)組頭部
- emitter.emit(eventName[, ...args]) 觸發(fā)類(lèi)型為 eventName 的監(jiān)聽(tīng)事件
- emitter.removeListener/off(eventName, listener) 移除類(lèi)型為 eventName 的監(jiān)聽(tīng)事件
- emitter.once(eventName, listener) 添加類(lèi)型為 eventName 的監(jiān)聽(tīng)事件,以后只能執(zhí)行一次并刪除
- emitter.removeAllListeners([eventName]) 移除全部類(lèi)型為 eventName 的監(jiān)聽(tīng)事件
正文
1. 初始化 init
_events 不存在時(shí),使用 Object.create(null) 來(lái)初始化,并把 _eventsCount 設(shè) 0。
劃重點(diǎn) —— Object.create(null) 可以創(chuàng)建一個(gè)沒(méi)有原型的對(duì)象。
為什么要用這種方法創(chuàng)建對(duì)象呢?開(kāi)發(fā)者這么做的目的其實(shí)還是出于性能上的考慮,因?yàn)?EventEmitter 在 Node.js 中應(yīng)用廣泛,為節(jié)省服務(wù)器內(nèi)存和執(zhí)行速度上不必要的開(kāi)銷(xiāo),肯定能省則省唄。
2. 添加事件綁定 addListener
首先判斷 target 的 _events 是否存在,如果不存在則還是用 Object.create(null) 創(chuàng)建。
如果存在,觸發(fā) newListener 類(lèi)型的事件。然后通過(guò) event[type] 找到已經(jīng)注冊(cè) type 類(lèi)型的監(jiān)聽(tīng)事件/監(jiān)聽(tīng)事件數(shù)組,并存到 existing 中。
如果該事件值為 undefined,則把直接把要注冊(cè)的監(jiān)聽(tīng)事件 listener 賦給不存在的事件。否則,更新事件數(shù)組(單一的 listener 要轉(zhuǎn)為數(shù)組)。
注意 prepend 的使用,可以靈活地把 listener 添加到監(jiān)聽(tīng)函數(shù)數(shù)組頭部或尾部。
3. 事件添加到數(shù)組頭部 prependListener
和 addListener 類(lèi)似,但是 prepend 為 true。2. 觸發(fā)事件 emit
若 handler 不存在,直接返回 false。
若 handler 是一個(gè)函數(shù),使用 Reflect 調(diào)用函數(shù)。如果是數(shù)組的話(huà)則遍歷數(shù)組并調(diào)用,然后返回 true。
3 移除事件綁定 removeListener
按 type 取出要?jiǎng)h除的監(jiān)聽(tīng)函數(shù)列表 list = event[type],當(dāng) list 等于要?jiǎng)h除的監(jiān)聽(tīng)函數(shù)時(shí),_eventsCount 減一后如果為 0,直接初始化 _events,否則只刪除當(dāng)前類(lèi)型的監(jiān)聽(tīng)函數(shù)。
接著往下看,若 typeof list !== 'function' 即 list 為數(shù)組時(shí),先確定要?jiǎng)h除監(jiān)聽(tīng)事件的位置 position,然后刪掉對(duì)應(yīng)的函數(shù)。
注意:為什么不用 list.splice(postion, 1) 而要專(zhuān)門(mén)寫(xiě)一個(gè) spliceOne 來(lái)刪除呢?
因?yàn)檫@個(gè)兩參數(shù)的方法要比內(nèi)置的 splice 可能快上 1.5 - 10 倍!我專(zhuān)門(mén)查看了下提交記錄,這個(gè)版本的方法經(jīng)過(guò)幾個(gè)開(kāi)發(fā)者改動(dòng)過(guò)最終成為現(xiàn)在這個(gè)樣子。不得不佩服各路大神對(duì)開(kāi)源的貢獻(xiàn)!至于 splice 為什么慢,我沒(méi)能查到原因,也許需要去看 v8 源碼。
4 事件只能執(zhí)行一次 once
這個(gè)方法的實(shí)現(xiàn)有點(diǎn) tricky,為了維護(hù) fired 的狀態(tài)它用到了閉包。
其它還有一些方法,我不再多寫(xiě)了,基本上原理就是這樣。有興趣的同學(xué)可以自己點(diǎn)擊前文的源碼鏈接查看。
下面是我抄 Node.js 的 EventEmitter 簡(jiǎn)單代碼實(shí)現(xiàn):
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();// 注冊(cè)所有事件 ee.once('wakeUp', (name) => { console.log(`${name}起來(lái)啦`); }); ee.on('eat', (name) => { console.log(`${name}吃饅頭啦`) }); ee.on('eat', (name) => { console.log(`${name}喝水啦`) }); const meetingFn = (name) => { console.log(`${name}開(kāi)早會(huì)啦`) }; ee.on('work', meetingFn); ee.on('work', (name) => { console.log(`${name}碼代碼啦`) });ee.emit('wakeUp', '子非'); ee.emit('wakeUp', '子非'); // 第二次沒(méi)有觸發(fā) ee.emit('eat', '子非'); ee.emit('work', '子非'); ee.off('work', meetingFn); // 移除開(kāi)會(huì)事件 ee.emit('work', '子非'); // 再次工作輸出: 子非起來(lái)啦 子非吃饅頭啦 子非喝水啦 子非開(kāi)早會(huì)啦 子非碼代碼啦 子非碼代碼啦 復(fù)制代碼總結(jié):
讀完 Node.js 的 EventEmitter 實(shí)現(xiàn),一些細(xì)節(jié)上的處理我覺(jué)得非常棒,而設(shè)計(jì)層面上,優(yōu)秀的包裝和抽象思路也讓我覺(jué)得十分經(jīng)典。EventEmitter 非常重要,很多大型庫(kù)像 Webpack,Socket.io 都是基于它來(lái)實(shí)現(xiàn)的,對(duì)于學(xué)習(xí) Js 的同學(xué)來(lái)說(shuō)是必須掌握它的。
歡迎溝通評(píng)論和交流!!!如果這篇文章幫助到了你,麻煩給個(gè)小心心哦??????
總結(jié)
以上是生活随笔為你收集整理的[原] 探索 EventEmitter 在 Node.js 中的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 使用Flutter之后,我们的CPU占用
- 下一篇: MyEclipse打不开 报xxxxx