Nodejs 的大部分核心 API 都是基於非同步事件驅動設計的,事件驅動核心是通過 node 中 Events 物件來實現事件的傳送和監聽回撥繫結,我們常用的 stream 模組也是依賴於 Events 模組是來實現資料流之間的回撥通知,如在資料到來時觸發 data 事件,流物件為可讀狀態觸發 readable 事件,當資料讀寫完畢後傳送 end 事件。
既然 Events 模組如此重要,我們有必要來學習一下 Events 模組的基本使用,以及如何模擬實現 Events 模組中常用的 api
一、Events 模組的基本使用以及簡單實現
首先我們瞭解一下 Events 模組的基本用法,其實 Events 模組本質上是觀察者模式的實現,所謂觀察者模式就是:
它定義了物件間的一種一對多的關係,讓多個觀察者物件同時監聽某一個主題物件,當一個物件發生改變時,所有依賴於它的物件都將得到通知
觀察者模式有對應的觀察者以及被觀察的物件,在 Events 模組中,對應的實現就是 on 和 emit 函式
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('嗨', (str) => {
console.log(str);
});
myEmitter.emit('嗨','你好');
複製程式碼
從上述的使用中,我們可以知道 on 是用來監聽事件的發生,而 emit 是用來觸發事件的發生,一旦 emit 觸發了事件,on 就會被通知到,從而執行對應的回撥函式。
有了這個例項,我們可以思考下如何實現這個 EventEmitter 類。
思路:當我們執行 on 函式時,我們可以將回撥函式儲存起來,等到 emit 觸發了事件時,將回撥函式拿出來執行,那麼就可以實現了事件的監聽以及訂閱了。
class EventEmitter{
constructor(){
#事件監聽函式儲存的地方
this.events={};
}
on(eventName,listener){
if (this.events[eventName]) {
this.events[eventName].push(listener);
} else {
#如果沒有儲存過,將回撥函式儲存為陣列
this.events[eventName] = [listener];
}
}
emit(eventName){
#emit觸發事件,把回撥函式拉出來執行
this.events[eventName] && this.events[eventName].forEach(listener => listener())
}
}
複製程式碼
上述就實現了一個簡單的 EventEmitter 類,下面來例項一下:
let event = new EventEmitter();
event.on('嗨',function(){
console.log('你好');
});
event.emit('嗨');
#輸出:你好
複製程式碼
完善:我們注意到在原生的 EventEmitter 類中,emit 是可以傳遞引數到我們的回撥函式中,那麼我們實現的類也應該支援傳遞引數。我們對 emit 進行如下更改
emit(eventName,...rest){
#emit觸發事件,把回撥函式拉出來執行
this.events[eventName] && this.events[eventName].forEach(listener => listener.apply(this,rest))
}
複製程式碼
完善之後,重新例項化,如下:
let event = new EventEmitter();
event.on('嗨',function(str){
console.log(str);
});
event.emit('嗨','你好');
#輸出:你好
複製程式碼
二、Events 模組中常用的 api
Events 模組中除了 on、emit 函式之外,還包含了很多常用的 api,我們一一來介紹幾個實用的 api
API名稱 | API方法描述 |
---|---|
addListener(eventName, listener) | on(eventName, listener)別名,為指定事件新增一個監聽器到監聽器陣列的尾部 |
removeListener(eventName, listener) | 從名為 eventName 的事件的監聽器陣列中移除指定的 listener |
removeAllListeners(eventName, listener) | 移除全部監聽器或指定的 eventName 事件的監聽器 |
once(eventName, listener) | 新增單次監聽器 listener 到名為 eventName 的事件 |
listeners(eventName) | 返回名為 eventName 的事件的監聽器陣列的副本 |
setMaxListeners(n) | 可以為指定的 EventEmitter 例項修改監聽器數量限制 |
1. addListener 與 on 方法使用與實現
在 Events 模組中,addListener 與 on 方法的使用是完成相同的,只是名字不同,我們可以通過原型來給兩個函式建立相等關聯
EventEmitter.prototype.addListener=EventEmitter.prototype.on
複製程式碼
2. removeListener 與 off 方法使用與實現
removeListener 方法可以從指定名字的監聽器陣列中移除指定的 listener,這樣的話,當再次 emit 事件的時候,不會觸發 on 繫結的回撥函式,如下:
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
let callback = (str) => {
console.log(str);
}
myEmitter.on('嗨', callback);
myEmitter.emit('嗨','你好');#輸出:你好
myEmitter.removeListener('嗨',callback);
myEmitter.emit('嗨','你好');#無輸出
複製程式碼
實現思路:我們只要在執行 removeListener 函式的時候,將先前儲存的回撥函式去除掉即可
removeListener (eventName,listener) {
#保證回撥函式陣列存在,同時去除指定的listener
this.events[eventName] && this.events[eventName] = this.events[eventName].filter(l => l != listener);
}
複製程式碼
同時 removeListener 與 off 方法也是功能完全相同,只是命名不同,因此可以通過如下方法賦值:
EventEmitter.prototype.removeListener=EventEmitter.prototype.off
複製程式碼
3. removeAllListeners 方法使用與實現
removeAllListeners 移除全部監聽器或指定的 eventName 事件的監聽器,其實 removeAllListeners 就包含了 removeListener 的功能,只是 removeListener 只能指定特定的監聽器,removeAllListeners 可以移除全部監聽器。 實現思路:在執行 removeAllListeners,將所有的回撥函式都給去除即可
removeAllListeners (eventName) {
#移除全部監聽器
delete this.events[eventName]
}
複製程式碼
4. once 方法使用與實現
once 方法的描述是新增單次監聽器 listener 到名為 eventName 的事件,其實就是通過 once 新增的監聽器,只能執行一次,執行一次之後就會被銷燬,不能再次執行
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.once('嗨', (str) => {
console.log(str);
});
myEmitter.emit('嗨','你好');
myEmitter.emit('嗨','你好');
myEmitter.emit('嗨','你好'); #只能輸出一次你好
複製程式碼
實現思路:當 once 監聽的事件回撥函式執行之後,通過 removeListener 將事件監聽器給解綁掉,那麼事件再次被 emit 的時候,就不會再次執行回撥,這樣就能保證事件回撥只能執行一次
once (eventName, listener) {
#重新改變監聽回撥函式,使其執行之後可以被銷燬
let reListener = (...rest) => {
listener.apply(this,rest);
#執行完之後解除事件繫結
this.removeListener(type,wrapper);
}
this.on(eventName,reListener);
}
複製程式碼
5. listeners 方法使用與實現
listeners 方法返回名為 eventName 的事件的監聽器陣列的副本,其實就是獲取 eventName 中所有的回撥函式,這個實現起來很容易,就不多贅述了,程式碼如下:
listeners (eventName) {
return this.events[eventName]
}
複製程式碼
6. setMaxListeners 方法使用與實現
預設情況下,如果為特定事件新增了超過 10 個監聽器,則 EventEmitter 會列印一個警告。 這有助於發現記憶體洩露, 但是,並不是所有的事件都要限制 10 個監聽器。 emitter.setMaxListeners() 方法可以為指定的 EventEmitter 例項修改限制。 值設為 Infinity(或 0)表示不限制監聽器的數量。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
let callback = (str) => {
console.log(str);
}
for (let i = 0; i <= 11; i++) {
myEmitter.on('嗨', callback);
}
myEmitter.emit('嗨', '你好');
複製程式碼
輸入結果如圖:
實現思路:- 我們先將特定事件的監聽器最大設定為常量10
constructor(){
#事件監聽函式儲存的地方
this.events={};
#最大監聽器數量
this._maxListeners = 10;
}
複製程式碼
- 然後在我們的 on 函式中,對這個監聽器的數量進行判斷,從而作出提示
on(eventName,listener){
if (this.events[eventName]) {
this.events[eventName].push(listener);
#如果超過最大限度,以及不為0,則作出記憶體洩漏提示
if (this._maxListeners != 0 && this.events[type].length >= this._maxListeners) {
console.error('超過最大的監聽數量可能會導致記憶體洩漏');
}
} else {
#如果沒有儲存過,將回撥函式儲存為陣列
this.events[eventName] = [listener];
}
}
複製程式碼
- 我們也支援對 _maxListeners 變數根據使用者的輸入進行更改,即我們的 setMaxListeners() 函式
setMaxListeners(MaxListeners) {
this._maxListeners = MaxListeners
}
複製程式碼
三、總結
本文從 node 的 Events 模組出發,然後去介紹了 Events 模組常用 API 的使用,從中通過一步一步簡易去思考這些 API 使用的內部原理,簡易的實現了這些 API,希望大家看完文章之後,能對 Events 模組有進一步的理解。