你有沒有想過,爲何瀏覽器的 div 上能夠綁定多個 onclick
事件,點擊一下 div 能夠觸發所有的事件,jquery 的 .on()
,.off()
,one()
又是如何實現的?Node.js 事件驅動的原理是怎樣的?node
實際上這一切都是 EventEmitter 在背後作支持,它是 JavaScript 經典的事件驅動實現,如今咱們來看下 Node.js 中是如何實現的。jquery
本文所說的監聽事件在實現上都爲函數,讀者能夠認爲二者相等以方便閱讀。git
由於原來的 Node 代碼量比較多,爲了方便演示,做者把本文的源代碼示例中涉及數據驗證,錯誤處理的部分刪除,保留了主要內容。github
內部屬性:數組
_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 的監聽事件_events
不存在時,使用 Object.create(null)
來初始化,並把 _eventsCount
設 0。bash
劃重點 —— Object.create(null)
能夠建立一個沒有原型的對象。服務器
爲何要用這種方法建立對象呢?開發者這麼作的目的其實仍是出於性能上的考慮,由於 EventEmitter 在 Node.js 中應用普遍,爲節省服務器內存和執行速度上沒必要要的開銷,確定能省則省唄。閉包
首先判斷 target
的 _events
是否存在,若是不存在則仍是用 Object.create(null)
建立。app
若是存在,觸發 newListener
類型的事件。而後經過 event[type]
找到已經註冊 type 類型的監聽事件/監聽事件數組,並存到 existing
中。
若是該事件值爲 undefined
,則把直接把要註冊的監聽事件 listener
賦給不存在的事件。不然,更新事件數組(單一的 listener
要轉爲數組)。
注意 prepend 的使用,能夠靈活地把 listener
添加到監聽函數數組頭部或尾部。
prepend
爲
true
。
若 handler 不存在,直接返回 false。
若 handler 是一個函數,使用 Reflect
調用函數。若是是數組的話則遍歷數組並調用,而後返回 true。
按 type
取出要刪除的監聽函數列表 list = event[type]
,當 list
等於要刪除的監聽函數時,_eventsCount
減一後若是爲 0,直接初始化 _events
,不然只刪除當前類型的監聽函數。
接着往下看,若 typeof list !== 'function'
即 list
爲數組時,先肯定要刪除監聽事件的位置 position
,而後刪掉對應的函數。
注意:爲何不用 list.splice(postion, 1)
而要專門寫一個 spliceOne
來刪除呢?
由於這個兩參數的方法要比內置的 splice
可能快上 1.5 - 10 倍!我專門查看了下提交記錄,這個版本的方法通過幾個開發者改動過最終成爲如今這個樣子。不得不佩服各路大神對開源的貢獻!至於 splice
爲何慢,我沒能查到緣由,也許須要去看 v8 源碼。
這個方法的實現有點 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 的同窗來講是必須掌握它的。
歡迎溝通評論和交流!!!若是這篇文章幫助到了你,麻煩給個當心心哦❤️❤️❤️