一文聊聊Node.js中的EventEmitter模組

2021-12-21 19:00:07
EventEmitter是 的內建模組,為我們提供了事件訂閱機制。下面本篇文章就來帶大家瞭解一下Node.js中的EventEmitter模組,介紹一下它的用法,希望對大家有所幫助!

EventEmitter 的使用

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

我們可以通過 onaddListener 方法來為某事件新增一個監聽器,二者的使用是一樣

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

從上面的例子也可以看出,可以為同一事件繫結多個回撥函數。

執行順序

當使用 onaddListener 繫結多個回撥函數時,觸發的順序就是新增的順序,如

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

offremoveListener 方法的作用同 onaddLsitener 的作用是相反的,它們的作用是為某個事件刪除對應的回撥函數

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 繫結的是一個匿名函數,那麼便無法通過 offremoveListener 去解綁一個回撥函數,因為它會通過比較兩個函數的參照是否相同來解綁函數的。

once

使用 once 可以繫結一個只執行一次的回撥函數,當觸發一次之後,該回撥函數便自動會被解綁

const {EventEmitter} = require("events");
const eventEmitter = new EventEmitter();

eventEmitter.once("data", () => {
    console.log("data");
});

eventEmitter.emit("data");
eventEmitter.emit("data");

上述程式碼我們使用 oncedata 事件繫結了一個回撥函數,然後使用 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

prependListener、prependOnceListener

使用 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

prependOnceListenerprependListener,不過它繫結的回撥函數只會被執行一次

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

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

通過 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

listenerCount 方法可以得到某個事件繫結了多少個回撥函數

const {EventEmitter} = require("events");
const eventEmitter = new EventEmitter();

eventEmitter.on("data", () => {

});
eventEmitter.on("data", () => {

});

console.log(eventEmitter.listenerCount("data")); // 2

setMaxLsiteners、getMaxListeners

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 個回撥函數,可以通過 EventEmitterdefaultMaxListeners 屬性獲得該值

const {EventEmitter} = require("events");

console.log(EventEmitter.defaultMaxListeners); // 10

listeners、rawListeners

當我們使用 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

在這個小節將從零實現一個 EventEmitter,來加深對該模組的理解。首先我們需要準備一個 listeners 來儲存所有繫結的回撥函數,它是一個 Map 物件,鍵是事件名,而值是一個陣列,陣列中儲存的是該事件繫結的回撥函數。

class EventEmitter {
    constructor() {
        this.listeners = new Map();
    }
}

on、addListener

使用 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

當我們使用 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

實現 prependListener 的邏輯同 on 一樣,不過我們是往陣列的最前方新增回撥函數

prependListener(event, callback) {
    if(!this.listeners.has(event)) {
        this.listeners.set(event, []);
    }
    let fns = this.listeners.get(event);
    fns.unshift(callback);
}

off、removeListener

使用 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);
    }
}

removeListeneroff 的作用一樣,我們在內部直接呼叫 off 方法即可

removeListener(event, callback) {
    this.off(event, callback);
}

once、prependOnceListener

使用 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

直接從刪除對應的事件,如果沒有傳入具體事件的話,則需要刪除所有的事件

removeAllListeners(event) {
    // 如果沒有傳入 event,則刪除所有事件
    if (event === undefined) {
        this.listeners = new Map();
        return;
    }
    this.listeners.delete(event);
}

eventNames

獲得已經繫結了哪些事件

eventNames() {
    return [...this.listeners.keys()];
}

listenerCount

獲得某事件繫結可多少個回撥函數

listenerCount(event) {
    return this.listeners.get(event).length;
}

上述的實現有一個 bug,那就是無法刪除使用 once 繫結的函數,我的想法是使用一個 Maponce 繫結的函數同對應的 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其它相關文章!