NodeJS中的事件(EventEmitter) API詳解(附原始碼)

煎蛋面__cq發表於2019-03-03

EventEmitter 簡介

EventEmitter 是 NodeJS 的核心模組 events 中的類,用於對 NodeJS 中的事件進行統一管理,用 events 特定的 API 對事件進行新增、觸發和移除等等,核心方法的模式類似於釋出訂閱。

實現 EventEmitter

1、EventEmitter 建構函式的實現

檔案:events.js
function EventEmitter() {
    this._events = Object.create(null);
}

/*
* 其他方法
*/

// 匯出自定義模組
module.export = EventEmitter;複製程式碼

在建構函式 EventEmitter 上有一個屬性 _events,型別為物件,用於儲存和統一管理所有型別的事件,在建立建構函式的時候匯出了 EventEmitter,後面實現其他方法的程式碼將放在建構函式與匯出中間。

2、事件最大監聽個數

在 EventEmitter 中監聽的每一類事件都有最大監聽個數,超過了這個數值,事件雖然可以正常執行,但是會發出警告資訊,其目的是為了防止記憶體洩露。

預設事件最大監聽個數
EventEmitter.defaultMaxListeners = 10;複製程式碼

這個同型別事件最大個數預設是 10,EventEmitter 當然也有方法設定和獲取這個值,下面是設定和獲取同型別事件最大監聽個數的方法實現。

操作最大事件監聽個數
// 設定同型別事件監聽最大個數
EventEmitter.prototype.setMaxListeners = function (count) {
    this._count = count;
}

// 獲取同型別事件監聽最大個數
EventEmitter.prototype.getMaxListeners = function () {
    return this._count || EventEmitter.defaultMaxListeners;
}複製程式碼

在設定這個值的時候其實就是給 EventEmitter 例項新增了一個 _count 的屬性用來儲存設定的新值來作為這個型別事件的最大監聽個數,在獲取的時候就是獲取 _count,如果沒有設定過就獲取預設值。

3、新增事件監聽

在給 EventEmitter 的例項新增事件監聽時,在 _event 物件中會以事件的型別作為屬性名,值為一個陣列,每次新增這個型別事件的時候,會將要執行的函式存入這個陣列中進行統一管理。

新增事件監聽的方法有 ononceaddListenerprependListenerprependOnceListener

  • on 等同於 addListener 將函式正常新增到 _event 對應事件型別的陣列中;
  • once 將函式新增到 _event 對應事件型別的陣列中,但是隻能執行一次;
  • prependListener 將函式新增到 _event 對應事件型別的陣列中的前面;
  • prependOnceListener 將函式新增到 _event 對應事件型別的陣列中的前面,但只能執行一次。

在 EventEmitter 中正常新增事件有四點需要注意:
1、如果其他的類使用 util 模組的 inherits 方法繼承 EventEmitter 時是無法繼承例項屬性的,在呼叫操作 _events 的方法中因為無法獲取到 _events 導致報錯,為了相容這種繼承的情況,在獲取不到 _events 時應新增一個 _events 到繼承 EventEmitter 的類的例項上;
2、如果新增事件的型別為 newListener,傳入要執行的函式會有一個引數 type ,是事件的型別,之後再新增事件的時候,就會執行 newListener 的函式,對新增的事件的事件型別進行處理;
3、on 方法表面上有兩個引數,實際上有第三個引數,為布林值,代表是否從 _events 對應事件型別的陣列前面追加函式成員;
4、在新增事件的時候需要判斷是否超出這個型別事件的最大監聽個數,如果超出要列印警告資訊。

on 方法和 addListener 方法的實現:

on 和 addListener 方法
// 新增事件監聽
EventEmitter.prototype.on = EventEmitter.prototype.addListener = function (type, callback, flag) {
    // 相容繼承不存在 _events 的情況
    if (!this._events) this._events = Object.create(null);

    // 如果 type 不是 newListener 就去執行 newListener 的回撥
    if (type !== "newListener") {
        // 如果沒新增過 newListener 事件就忽略此處的邏輯
        if (this._events["newListener"] && this._events["newListener"].length) {
            this._events["newListener"].forEach(fn => fn(type));
        }
    }

    // 如果不是第一次新增 callback 存入陣列中
    if (this._events[type]) {
        // 是否從陣列前面新增 callback
        if (flag) {
            this._events[type].unshift(callback);
        } else {
            this._events[type].push(callback);
        }
    } else {
        // 第一次新增,在 _events 中建立陣列並新增 callback 到陣列中
        this._events[type] = [callback];
    }

    // 獲取事件最大監聽個數
    let maxListeners = this.getMaxListeners();

    // 判斷 type 型別的事件是否超出最大監聽個數,超出列印警告資訊
    if (this._events[type].length - 1 === maxListeners) {
        console.error(`MaxListenersExceededWarning: ${maxListeners + 1} ${type} listeners added`);
    }
}複製程式碼

通過上面程式碼可以看出 on 方法的第三個引數其實是服務於 prependListener 方法的,其他新增事件的方法都是基於 on 來實現的,只是在呼叫 on 的外層做了不同的處理,而我們平時調這些新增事件監聽的方法時都只傳入 typecallback

prependListener 方法的實現:

prependListener 方法
// 新增事件監聽,從陣列的前面追加
EventEmitter.prototype.prependListener = function (type, callback) {
    // 第三個引數為 true 表示從 _events 對應事件型別的陣列前面新增 callback
    this.on(type, callback, true);
}複製程式碼

once 方法的實現:

once 方法
// 新增事件監聽,只能執行一次
EventEmitter.prototype.once = function (type, callback, flag) {
    let wrap => (...args) {
        callback(...args);

        // 執行 callback 後立即從陣列中移除 callback
        this.removeListener(type, wrap);
    }

    // 儲存 callback,確保單獨使用 removeListener 刪除傳入的 callback 時可以被刪除掉
    wrap.realCallback = callback;

    // 呼叫 on 新增事件監聽
    this.on(type, wrap, flag);
}複製程式碼

想讓事件只執行一次,需要在執行 callback 之後就立即在陣列中移除這個函式,由於是同步執行,直接操作 callback 是很難實現的,新增事件其實就是新增 callback_events 對應型別的陣列中,我們在使用 once 的時候將 callback 包一層函式名為 wrap,將這個外層函式存入陣列,wrap 的內部邏輯就是真正 callback 的呼叫和移除 wrap,這裡涉及到事件監聽的移除方法 removeListener 在後面來詳細說明。

once 的第三個引數是為了 prependOnceListener 服務的,prependOnceListenerprependListener實現方式類似,不同的是 prependOnceListener 是基於 once 實現的。

prependOnceListener 方法的實現:

prependOnceListener 方法
// 新增事件監聽,從陣列的前面追加,只執行一次
EventEmitter.prototype.prependOnceListener = function (type, callback) {
    // 第三個引數為 true 表示從 _events 對應事件型別的陣列前面新增 callback
    this.once(type, callback, true);
}複製程式碼

4、移除事件監聽

移除事件監聽有兩個方法,分別是 removeListenerremoveAllListeners,前者的作用是移除某個型別陣列中的某個回撥函式,後者的作用是移除某個型別陣列的所有成員,如果型別引數為空,則清空整個 _events

removeListener 方法的實現:

removeListener 方法
// 移除事件執行程式
EventEmitter.prototype.removeListener = function (type, callback) {
    if(this._events[type]) {
        // 過濾掉當前傳入的要移除的 callback
        this._events[type] = this._events[type].filter(fn => {
            return fn !== callback && fn !== callback.realCallback;
        });
    }
}複製程式碼

由於 once 中在真正的 callback 包了一層 wrap, 只有在觸發事件時才能執行 wrap 並執行 removeListener 刪掉函式,如果在事件觸發之前使用 removeListener 刪除,傳入的是真正的回撥 callback,無法刪除,所以在 once 方法中對真正的 callback 進行了儲存,在 removeListener 中呼叫 filter 時的返回條件的邏輯中做了處理。

removeAllListeners 方法的實現:

removeAllListeners 方法
// 移除全部事件執行程式
EventEmitter.prototype.removeAllListeners = function (type) {
    // 存在 type 清空 _events 對應的陣列,否則直接清空 _events
    if (type) {
        this._events[type] = [];
    } else {
        this._events = Object.create(null);
    }
}複製程式碼

5、觸發事件監聽

執行事件就比較簡單了,取出 _events 中對應型別的陣列進行迴圈,執行內部的每一個函式,第一個引數為 type,後面引數會作為陣列中函式執行傳入的引數。

emit 方法
// 觸發事件
EventEmitter.prototype.emit = function (type, ...args) {
    if (this._events[type]) {
        // 迴圈執行函式,並將 this 指回 EventEmitter 例項
        this._events[type].forEach(fn => fn.call(this, ...args));
    }
}複製程式碼

6、獲取事件型別名稱集合

eventNames 方法
// 獲取監聽的所有事件型別
EventEmitter.prototype.eventNames = function () {
    return Object.keys(this._events);
}複製程式碼

7、按事件型別獲取執行程式的集合

listeners 方法
// 獲取事件型別對應的陣列
EventEmitter.prototype.listeners = function (type) {
    return this._events[type];
}複製程式碼

EventEmitter 的基本使用

EventEmitter 的核心邏輯已經實現,由於上面大多數方法需要組合使用,所以在沒有一一驗證,下面讓我們通過一些案例來了解 EventEmitter 的用法。

我們在這裡引入自己自定義的 events 模組,並使用 util 模組的 inherits 繼承 EventEmitter,下面是前置程式碼,後面將不在重複。

檔案:events-demo.js
// 引入依賴
const EventEmitter = require("./events");
const util = require("util");

function Girl() {}

// 使 Girl 繼承 EventEmitter
util.inherits(Girl, EventEmitter);

// 建立 Girl 的例項
let girl = new Girl();複製程式碼

案例 1:設定和獲取同型別事件的最大監聽個數

檔案:events-demo.js
// 獲取事件最大監聽個數
console.log(girl.getMaxListeners()); // 10

// 設定事件最大監聽個數
girl.setMaxListeners(2);
console.log(girl.getMaxListeners()); // 2複製程式碼

案例 2:使用 on 新增事件並執行

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.on("失戀", () => console.log("喝酒"));

girl.emit("失戀");

// 哭了
// 喝酒複製程式碼

案例 3:使用 prependListener 新增事件並執行

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.prependListener("失戀", () => console.log("喝酒"));

girl.emit("失戀");

// 喝酒
// 哭了複製程式碼

案例 4:新增 newListener 型別的事件

檔案:events-demo.js
girl.on("newListener", (type) => console.log(type));

girl.on("失戀", () => console.log("哭了"));
girl.on("和好", () => console.log("開心"));

// 失戀
// 和好複製程式碼

案例 5:新增同型別事件超出最大個數並執行事件

檔案:events-demo.js
// 設定事件最大監聽個數
girl.setMaxListeners(2);

girl.on("失戀", () => console.log("哭了"));
girl.on("失戀", () => console.log("喝酒"));
girl.on("失戀", () => console.log("吸菸"));

girl.emit("失戀");

// MaxListenersExceededWarning: 3 失戀 listeners added
// 哭了
// 喝酒
// 吸菸複製程式碼

案例 6:對比 on 和 once

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.once("失戀", () => console.log("喝酒"));

girl.emit("失戀");
girl.emit("失戀");

// 哭了
// 喝酒
// 哭了複製程式碼

案例 7:移除 on 和 once 新增的事件監聽

檔案:events-demo.js
let cry = () => console.log("哭了");
let drink = () => console.log("喝酒");

girl.on("失戀", cry);
girl.once("失戀", drink);
girl.on("失戀", () => console.log("吸菸"));

girl.removeListener("失戀", cry);
girl.removeListener("失戀", drink);

// 吸菸複製程式碼

案例 8:使用 prependOnceListener 新增事件監聽

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.prependOnceListener("失戀", () => console.log("喝酒"));

girl.emit("失戀");
girl.emit("失戀");

// 喝酒
// 哭了
// 哭了複製程式碼

案例 9:獲取某個事件型別執行程式的集合

檔案:events-demo.js
let cry = () => console.log("哭了");
let drink = () => console.log("喝酒");

girl.on("失戀", cry);
girl.once("失戀", drink);
girl.once("失戀", () => console.log("吸菸"));

console.log(girl.listeners("失戀"));

// [ [Function: cry], [Function: drink], [Function] ]複製程式碼

案例 10:獲取所有事件型別名稱

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.on("和好", () => console.log("開心"));

console.log(girl.eventNames());

// [ `失戀`, `和好` ]複製程式碼

案例 11:使用 removeAllListeners 按型別移除事件監聽

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.on("失戀", () => console.log("喝酒"));
girl.on("和好", () => console.log("開心"));

// 移除 “失戀” 型別事件監聽
girl.removeAllListeners("失戀");

console.log(girl.listeners("失戀"));

// []複製程式碼

案例 12:使用 removeAllListeners 移除全部事件監聽

檔案:events-demo.js
girl.on("失戀", () => console.log("哭了"));
girl.on("失戀", () => console.log("喝酒"));
girl.on("和好", () => console.log("開心"));

// 移除全部事件監聽
girl.removeAllListeners();

console.log(girl._events);

// {}複製程式碼

EventEmitter 總結

events 模組在 NodeJS 中的使用率非常高,很多其他模組的事件執行機制都是通過繼承該模組的 EventEmitter 類來實現的,比如 ReadStream(可讀流)、WriteStream(可寫流)、net(tcp)和 http 等等,我們也可以通過上面案例的方式建立自己的類去繼承 EventEmitter 來實現事件的管理。

原文出自:https://www.pandashen.com

相關文章