事件系統 (EventEmitter)#
Node.js 的核心架構基於事件驅動(Event-Driven) 模型:程式不是依照固定順序執行,而是等待事件發生,再執行對應的處理函式。
這個模型由兩個角色組成:
- 發佈者(Emitter):在某件事發生時觸發事件
- 訂閱者(Listener):事先登記「當某個事件發生時,要執行什麼」
events 模組提供的 EventEmitter 類別實作了這個機制,Node.js 許多內建模組(如 fs、http、stream)都繼承自它。
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!
|
執行流程:
on() 把監聽函式登記到 "greet" 這個事件名稱下emit() 觸發 "greet",EventEmitter 找出所有登記的函式並依序呼叫- 傳給
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#