事件系統 (EventEmitter)#

Node.js 的核心架構基於事件驅動(Event-Driven) 模型:程式不是依照固定順序執行,而是等待事件發生,再執行對應的處理函式

這個模型由兩個角色組成:

  • 發佈者(Emitter):在某件事發生時觸發事件
  • 訂閱者(Listener):事先登記「當某個事件發生時,要執行什麼」

events 模組提供的 EventEmitter 類別實作了這個機制,Node.js 許多內建模組(如 fshttpstream)都繼承自它。


1. 基本使用#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const EventEmitter = require("events");

const emitter = new EventEmitter();

// 訂閱:登記當 "greet" 事件發生時,執行這個函式
emitter.on("greet", (name) => {
    console.log(`Hello, ${name}!`);
});

// 發佈:觸發 "greet" 事件,並傳入參數
emitter.emit("greet", "Node.js"); // Hello, Node.js!
emitter.emit("greet", "Alice");   // Hello, Alice!

執行流程:

  1. on() 把監聽函式登記到 "greet" 這個事件名稱下
  2. emit() 觸發 "greet",EventEmitter 找出所有登記的函式並依序呼叫
  3. 傳給 emit() 的額外引數,會原封不動地傳給監聽函式

同一個事件可以登記多個監聽器,觸發時會依登記順序全部執行:

1
2
3
4
5
6
emitter.on("start", () => console.log("監聽器 A"));
emitter.on("start", () => console.log("監聽器 B"));

emitter.emit("start");
// 監聽器 A
// 監聽器 B

2. 只執行一次(once)#

once() 登記的監聽器只會執行一次,觸發後自動移除:

1
2
3
4
5
6
emitter.once("connect", () => {
    console.log("已連線!");
});

emitter.emit("connect"); // 已連線!
emitter.emit("connect"); // 沒有輸出(監聽器已被移除)

適合用在只需要處理一次的場景,例如初始化完成通知、一次性的連線確認。


3. 移除監聽器(off)#

若不再需要監聽某個事件,應主動移除,避免記憶體洩漏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function onData(data) {
    console.log("收到資料:", data);
}

emitter.on("data", onData);

emitter.emit("data", "第一筆"); // 收到資料:第一筆

// 移除監聽器(需傳入與 on() 相同的函式參考)
emitter.off("data", onData);

emitter.emit("data", "第二筆"); // 沒有輸出

注意:移除時必須傳入同一個函式參考,因此監聽器不能用匿名函式:

1
2
3
4
5
6
7
8
// 無法移除:每次 function() {} 都是不同的參考
emitter.on("data", function(data) { console.log(data); });
emitter.off("data", function(data) { console.log(data); }); // 無效!

// 正確做法:先將函式存成變數
const handler = (data) => console.log(data);
emitter.on("data", handler);
emitter.off("data", handler); // 有效

移除全部監聽器:

1
2
emitter.removeAllListeners("data"); // 移除 "data" 的所有監聽器
emitter.removeAllListeners();       // 移除所有事件的所有監聽器

4. 繼承 EventEmitter#

實際開發中,通常讓自己的類別繼承 EventEmitter,讓物件本身具備發佈事件的能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const EventEmitter = require("events");

class Timer extends EventEmitter {
    start(seconds) {
        let count = 0;
        const interval = setInterval(() => {
            count++;
            this.emit("tick", count);     // 每秒發佈 tick 事件

            if (count >= seconds) {
                clearInterval(interval);
                this.emit("done");        // 結束時發佈 done 事件
            }
        }, 1000);
    }
}

const timer = new Timer();

// 在外部訂閱事件,Timer 內部不需要知道誰在監聽
timer.on("tick", (sec) => {
    console.log(`已過 ${sec} 秒`);
});

timer.on("done", () => {
    console.log("計時結束!");
});

timer.start(3);
// 已過 1 秒
// 已過 2 秒
// 已過 3 秒
// 計時結束!

這種設計讓 Timer 與外部程式碼鬆耦合:Timer 只負責發佈事件,不需要關心誰來處理、怎麼處理。


5. 錯誤事件(error)#

error 是一個特殊事件。若觸發 error 時沒有對應的監聽器,Node.js 會拋出例外並終止程式

1
2
3
4
5
const emitter = new EventEmitter();

// 沒有 error 監聽器的情況下觸發 error
emitter.emit("error", new Error("未預期的錯誤"));
// 程式崩潰!UnhandledError: 未預期的錯誤

正確做法是永遠加上 error 監聽器:

1
2
3
4
5
6
7
emitter.on("error", (err) => {
    console.error("發生錯誤:", err.message);
    // 在這裡做錯誤處理,程式不會崩潰
});

emitter.emit("error", new Error("連線中斷"));
// 發生錯誤:連線中斷

6. 監聽器數量上限#

預設每個事件最多只能登記 10 個監聽器,超過會印出警告(不是錯誤):

1
2
3
4
5
6
7
8
// 調整上限(設為 0 表示不限制)
emitter.setMaxListeners(20);

// 查詢目前監聽器數量
console.log(emitter.listenerCount("data")); // 1

// 查詢所有已登記的事件名稱
console.log(emitter.eventNames()); // ["data", "error"]

7. 常用方法整理#

方法說明
on(event, fn)登記監聽器(可多次觸發)
once(event, fn)登記只執行一次的監聽器
off(event, fn)移除指定監聽器
emit(event, ...args)觸發事件,傳入參數給監聽器
removeAllListeners(event)移除指定事件的所有監聽器
listenerCount(event)取得指定事件的監聽器數量
eventNames()取得所有已登記的事件名稱
setMaxListeners(n)設定每個事件的監聽器數量上限

Reference#