EventEmitter 為我們提供了事件訂閱機制,通過引入 events
模組來使用它。
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); // 監聽 data 事件 eventEmitter.on("data", () => { console.log("data"); }); // 觸發 data 事件 eventEmitter.emit("data");
上述程式碼我們使用 on
方法來為事件繫結回撥函數,使用 emit
方法來觸發一個事件。
我們可以通過 on
和 addListener
方法來為某事件新增一個監聽器,二者的使用是一樣
eventEmitter.on("data", () => { console.log("data"); }); eventEmitter.addListener("data", () => { console.log("data"); });
第一個引數為事件名,第二個引數為對應的回撥函數,當 EventEmitter 範例物件呼叫 emit
觸發相應的事件時便會呼叫該回撥函數,如
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("data"); }); eventEmitter.addListener("data", () => { console.log("data"); }); eventEmitter.emit("data");
在控制檯會列印出兩次 data
data data
從上面的例子也可以看出,可以為同一事件繫結多個回撥函數。
當使用 on
或 addListener
繫結多個回撥函數時,觸發的順序就是新增的順序,如
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("data 1"); }); eventEmitter.on("data", () => { console.log("data 2"); }); eventEmitter.on("data", () => { console.log("data 3"); }); eventEmitter.emit("data");
會在控制檯依次列印出
data 1 data 2 data 3
並且使用 on
方法系結事件時,並不會做去重檢查
const {EventEmitter} = require('events'); const eventEmitter = new EventEmitter(); const listener = () => { console.log("lsitener"); } eventEmitter.on("data", listener); eventEmitter.on("data", listener); eventEmitter.emit("data");
控制檯的列印結果為
lsitener lsitener
上面的程式為事件繫結了兩次 listener
這個函數,但是內部並不會檢查是否已經新增過這個回撥函數,然後去重,所以上面在控制檯列印出了兩次 listener。
另外回撥函數還可以接收引數,引數通過 emit
觸發事件時傳入,如
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("data", data => { console.log(data); }); // 為回撥函數傳入引數 HelloWorld! eventEmitter.emit("data", "HelloWorld!");
上面我們使用 emit
觸發事件時,還傳遞了額外的引數,這個引數會被傳遞給回撥函數。
另外一個比較關心的問題,事件的觸發是同步的還是非同步的,我們做一個實驗
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("觸發了 data 事件!"); }); console.log("start"); eventEmitter.emit("data"); console.log("end");
上面我們我們在觸發事件前後都向控制檯列印了資訊,如果觸發事件後是非同步執行的,那麼後面的列印語句就會先執行,否則如果是同步的話,就會先執行事件繫結的回撥函數。執行結果如下
start 觸發了 data 事件! end
可見事件觸發是同步執行的。
off
與 removeListener
方法的作用同 on
和 addLsitener
的作用是相反的,它們的作用是為某個事件刪除對應的回撥函數
const {EventEmitter} = require('events'); const eventEmitter = new EventEmitter(); let listener1 = () => { console.log("listener1"); } let listener2 = () => { console.log("listener2"); } eventEmitter.on("data", listener1); eventEmitter.on("data", listener2); // 第一次觸發,兩個回撥函數否會執行 eventEmitter.emit("data"); eventEmitter.off("data", listener1); // 第二次觸發,只會執行 listener2 eventEmitter.emit("data");
控制檯列印結果為
listener1 listener2 listener2
第一次觸發事件時,兩個事件都會觸發,然後我們為事件刪除了 listener1 這個回撥函數,所以第二次觸發時,只會觸發 listener2。
注意:如果我們使用
on
或者addListener
繫結的是一個匿名函數,那麼便無法通過off
和removeListener
去解綁一個回撥函數,因為它會通過比較兩個函數的參照是否相同來解綁函數的。
使用 once
可以繫結一個只執行一次的回撥函數,當觸發一次之後,該回撥函數便自動會被解綁
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.once("data", () => { console.log("data"); }); eventEmitter.emit("data"); eventEmitter.emit("data");
上述程式碼我們使用 once
為 data
事件繫結了一個回撥函數,然後使用 emit
方法觸發了兩次,因為使用 once
繫結的回撥函數只會被觸發一次,所以第二次觸發,回撥函數不會執行,所以在控制檯只列印了一次 data。
另外同 on
繫結的回撥函數一樣,我們同樣可以通過 emit
方法向回撥函數傳遞引數
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.once("data", data => { console.log(data); }); eventEmitter.emit("data", "Hello");
控制檯列印結果
Hello
使用 on
或者 addListener
為事件繫結的回撥函數會被根據新增的順序執行,而使用 prependLsitener
繫結的事件回撥函數會在其他回撥函數之前執行
const {EventEmitter} = require('events'); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("on"); }); eventEmitter.prependListener("data", () => { console.log("prepend"); }); eventEmitter.emit("data");
上述代打我們先用控制檯的列印結果為
prepend on
prependOnceListener
同 prependListener
,不過它繫結的回撥函數只會被執行一次
const {EventEmitter} = require('events'); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("on"); }); eventEmitter.prependOnceListener("data", () => { console.log("prepend once"); }); eventEmitter.emit("data"); eventEmitter.emit("data");
上面我們使用 prependOnceListener
繫結了一個回撥函數,當觸發事件時,該回撥函數會在其他函數之前執行,並且只會執行一次,所以當第二次我們觸發函數時,該回撥函數不會執行,控制檯列印結果為
prepend once on on
removeAllListeners([event])
方法可以刪除事件 event
繫結的所有回撥函數,如果沒有傳入 event
引數的話,那麼該方法就會刪除所有事件繫結的回撥函數
const {EventEmitter} = require('events'); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { console.log("data 1"); }); eventEmitter.on("data", () => { console.log("data 2"); }); eventEmitter.emit("data"); eventEmitter.removeAllListeners("data"); eventEmitter.emit("data");
上面程式為 data
事件繫結了兩個回撥函數,並且在呼叫 removeAllListeners
方法之前分別觸發了一次 data
事件,第二次觸發 data
事件時,不會有任何的回撥函數被執行,removeAllListeners
刪除了 data
事件繫結的所有回撥函數。控制檯的列印結果為:
data 1 data 2
通過 eventNames
方法我們可以知道為哪些事件繫結了回撥函數,它返回一個陣列
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("start", () => { console.log("start"); }); eventEmitter.on("end", () => { console.log("end"); }); eventEmitter.on("error", () => { console.log("error"); }); console.log(eventEmitter.eventNames()); // [ 'start', 'end', 'error' ]
如果我們將某事件的所有回撥函數刪除後,此時 eventNames
便不會返回該事件了
eventEmitter.removeAllListeners("error"); console.log(eventEmitter.eventNames()); // [ 'start', 'end' ]
listenerCount
方法可以得到某個事件繫結了多少個回撥函數
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.on("data", () => { }); eventEmitter.on("data", () => { }); console.log(eventEmitter.listenerCount("data")); // 2
setMaxListeners
是用來設定最多為每個事件繫結多少個回撥函數,但是實際上是可以繫結超過設定的數目的回撥函數的,不過當你係結超過指定數目的回撥函數時,會在控制檯給出一個警告
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); // 設定只能為每個回撥函數繫結 1 個回撥函數 eventEmitter.setMaxListeners(1); // 為 data 事件繫結了三個回撥函數 eventEmitter.on("data", () => { console.log("data 1"); }); eventEmitter.on("data", () => { console.log("data 2"); }); eventEmitter.on("data", () => { console.log("data 3"); });
執行上述程式,控制檯列印結果為
data 1 data 2 data 3 (node:36928) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 2 data listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit
可見事件繫結的三個回撥函數都可以被觸發,並且在控制檯列印出了一條警告資訊。
getMaxListeners
是獲得能為每個事件繫結多少個回撥函數的方法,使用 setMaxListeners
設定的值時多少,返回的值就是多少
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.setMaxListeners(1); console.log(eventEmitter.getMaxListeners()); // 1
如果沒有使用 setMaxLsiteners
進行設定,那麼預設能夠為每個事件最多繫結 10
個回撥函數,可以通過 EventEmitter
的 defaultMaxListeners
屬性獲得該值
const {EventEmitter} = require("events"); console.log(EventEmitter.defaultMaxListeners); // 10
當我們使用 once
繫結一個回撥函數時,不會直接為該事件繫結該函數,而是會使用一個函數包裝該函數,這個包裝函數稱為 wrapper
,然後為該事件繫結 wrapper
函數,在 wrapper
函數內部,設定了當執行一次之後將自己解綁的邏輯。
listeners
返回指定事件繫結的回撥函陣列成的陣列,而 rawListeners
也是返回指定事件繫結的回撥函陣列成的陣列,與 listeners
不同的是,對於 once
繫結的回撥函數返回的是 wrapper
,而不是原生繫結的函數。
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.once("data", () => { console.log("once"); }) let fns = eventEmitter.listeners("data"); // once 繫結的函數,不是 wrapper,內部沒有解綁的邏輯,所以後面觸發 data 事件時還會執行 once 繫結的函數 fns[0]() eventEmitter.emit("data");
控制檯列印結果為
once once
下面將上面的 listeners
替換為 rawListeners
const {EventEmitter} = require("events"); const eventEmitter = new EventEmitter(); eventEmitter.once("data", () => { console.log("once"); }) let fns = eventEmitter.rawListeners("data"); // 因為返回的是 once 繫結函數的 wrapper,其內部有執行一次後解綁的邏輯 // 所以後面觸發事件時 once 繫結的函數不會再執行 fns[0]() eventEmitter.emit("data");
控制檯的列印結果為
once
在這個小節將從零實現一個 EventEmitter,來加深對該模組的理解。首先我們需要準備一個 listeners
來儲存所有繫結的回撥函數,它是一個 Map
物件,鍵是事件名,而值是一個陣列,陣列中儲存的是該事件繫結的回撥函數。
class EventEmitter { constructor() { this.listeners = new Map(); } }
使用 on
繫結回撥函數時,我們先判斷 Map
集合中是否有為該事件繫結回撥函數,如果有取出對應陣列,並新增該回撥函數進陣列,沒有則新建一個陣列,新增該回撥函數,並新增進 Map
集合
on(event, callback) { if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.push(callback); }
addListener
的功能與 on
是一樣的,我們直接呼叫 on
方法即可
addListener(event, callback) { this.on(event, callback); }
當我們使用 emit
觸發事件時,我們從 Map
取出對應的回撥函陣列成的陣列,然後依次取出函數執行。另外我們還可以通過 emit
傳遞引數
emit(event, ...args) { if(!this.listeners.has(event)) { return; } let fns = this.listeners.get(event); let values = []; for(let fn of fns) { values.push(fn); } for (let fn of values) { fn(...args); } }
這裡你可能會覺得我寫的有點複雜,所以你會覺得直接這麼寫更好
emit(event, ...args) { if(!this.listeners.has(event)) { return; } for (let fn of fns) { fn(...args); } }一開始我也是這麼寫的,但是因為
once
繫結的函數它在執行完畢後將自己從陣列中移除,並且是同步的,所以在執行迴圈的時候,陣列是在不斷變化的,使用上述的方式會使得一些回撥函數會被漏掉,所以我才會先將陣列中的函數複製到另一個陣列,然後遍歷這個新的陣列,因為once
繫結的函數它只會刪除原陣列中的函數,而不會刪除新的這個陣列,所以新陣列的長度在遍歷的過程不會改變,也就不會發生漏掉函數未執行的情況。
實現 prependListener
的邏輯同 on
一樣,不過我們是往陣列的最前方新增回撥函數
prependListener(event, callback) { if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.unshift(callback); }
使用 off
方法是用來解綁事件的,在陣列中找到指定的函數,然後刪除即可
off(event, callback) { if(!this.listeners.has(event)) { return; } let fns = this.listeners.get(event); // 找出陣列中的回撥函數,然後刪除 for (let i = 0; i < fns.length; i++) { if(fns[i] === callback) { fns.splice(i, 1); break; } } // 如果刪除回撥函數後,陣列為空,則刪除該事件 if (fns.length === 0) { this.listeners.delete(event); } }
removeListener
同 off
的作用一樣,我們在內部直接呼叫 off
方法即可
removeListener(event, callback) { this.off(event, callback); }
使用 once
繫結一個只執行一次的函數,所以我們需要將繫結的回撥函數使用一個函數包裝一下,然後新增進陣列中,這個包裝函數我們稱之為 wrapper
。在包裝函數中,當執行一遍後會將自己從陣列中刪除
once(event, callback) { let wrapper = (...args) => { callback(...args); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.push(wrapper); }
prependOnceListener
的實現同 once
,只是向陣列的開頭插入函數,將上面程式碼中的 push
換為 unshift
即可
prependOnceListener(event, callback) { let wrapper = (...args) => { callback(...args); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.unshift(wrapper); }
直接從刪除對應的事件,如果沒有傳入具體事件的話,則需要刪除所有的事件
removeAllListeners(event) { // 如果沒有傳入 event,則刪除所有事件 if (event === undefined) { this.listeners = new Map(); return; } this.listeners.delete(event); }
獲得已經繫結了哪些事件
eventNames() { return [...this.listeners.keys()]; }
獲得某事件繫結可多少個回撥函數
listenerCount(event) { return this.listeners.get(event).length; }
上述的實現有一個 bug,那就是無法刪除使用
once
繫結的函數,我的想法是使用一個Map
將once
繫結的函數同對應的wrapper
對應,刪除時即可根據once
的回撥函數找到對應的wrapper
然後刪除constructor() { this.listeners = new Map(); // 儲存 once 的回撥函數與對應的 wrapper this.onceToWrapper = new Map(); } once(event, callback) { let wrapper = (...args) => { callback(...args); // 刪除之前,刪除 callback 和 wrapper 的關係 this.onceToWrapper.delete(callback); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); // 新增之前,繫結 callback 和 wrapper 的關係 this.onceToWrapper.set(callback, wrapper); fns.push(wrapper); } prependOnceListener(event, callback) { let wrapper = (...args) => { callback(...args); // 同上 this.onceToWrapper.delete(callback); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); // 同上 this.onceToWrapper.set(callback, wrapper); fns.unshift(wrapper); } off(event, callback) { if(!this.listeners.has(event)) { return; } let fns = this.listeners.get(event); // 先從 onceToWrapper 中查詢是否有對應的 wrapper,如果有說明是 once 繫結的 callback = this.onceToWrapper.get(callback) || callback; for (let i = 0; i < fns.length; i++) { if(fns[i] === callback) { fns.splice(i, 1); break; } } if (fns.length === 0) { this.listeners.delete(event); } }
全部程式碼如下
class EventEmitter { constructor() { this.listeners = new Map(); this.onceToWrapper = new Map(); } on(event, callback) { if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.push(callback); } addListener(event, callback) { this.on(event, callback); } emit(event, ...args) { if(!this.listeners.has(event)) { return; } let fns = this.listeners.get(event); let values = []; for(let fn of fns) { values.push(fn); } for (let fn of values) { fn(...args); } } prependListener(event, callback) { if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); fns.unshift(callback); } off(event, callback) { if(!this.listeners.has(event)) { return; } let fns = this.listeners.get(event); callback = this.onceToWrapper.get(callback) || callback; for (let i = 0; i < fns.length; i++) { if(fns[i] === callback) { fns.splice(i, 1); break; } } if (fns.length === 0) { this.listeners.delete(event); } } removeListener(event, callback) { this.off(event, callback); } once(event, callback) { let wrapper = (...args) => { callback(...args); this.onceToWrapper.delete(callback); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); this.onceToWrapper.set(callback, wrapper); fns.push(wrapper); } prependOnceListener(event, callback) { let wrapper = (...args) => { callback(...args); this.onceToWrapper.delete(callback); this.off(event, wrapper); } if(!this.listeners.has(event)) { this.listeners.set(event, []); } let fns = this.listeners.get(event); this.onceToWrapper.set(callback, wrapper); fns.unshift(wrapper); } removeAllListeners(event) { if (event === undefined) { this.listeners = new Map(); return; } this.listeners.delete(event); } eventNames() { return [...this.listeners.keys()]; } listenerCount(event) { return this.listeners.get(event).length; } }
更多node相關知識,請存取:!!
以上就是一文聊聊Node.js中的EventEmitter模組的詳細內容,更多請關注TW511.COM其它相關文章!