Node.js 事件与 EventEmitter
Node 是"事件驱动"的——网络请求到来、文件读完、定时器触发,本质上都是事件。而 events 模块就是这套机制的基石。几乎所有 Node 内置模块(stream、http、fs)都基于 EventEmitter 实现。掌握它,你才算真正理解 Node 的事件驱动模型。
1. EventEmitter 基础
EventEmitter 是一个发布/订阅模式的实现。核心 API 极简——on 注册监听器,emit 触发事件:
const EventEmitter = require("events");
// 继承 EventEmitter,或直接 new 一个实例
const ee = new EventEmitter();
// 注册监听器:on(事件名, 回调)
ee.on("greet", (name) => {
console.log("你好," + name);
});
// 另一个监听器:同一个事件可以注册多个
ee.on("greet", (name) => {
console.log("Hello, " + name);
});
// 触发事件:emit(事件名, ...参数)
ee.emit("greet", "小明");
// 输出:
// 你好,小明
// Hello, 小明
// (监听器按注册顺序依次执行)关键认知:
- 事件名是字符串,自定义任意名字(如
"greet"、"data"、"error"、"end")。 - 同步执行:
emit会同步依次调用所有监听器,不是异步的。所以监听器里别做太重的活。 - 一个事件可以挂多个监听器,按注册顺序执行。
emit的参数会原样传给每个监听器。
2. once、off、其他常用方法
const ee = new EventEmitter();
// once:只监听一次,触发后自动移除
ee.once("init", () => {
console.log("初始化(只执行一次)");
});
ee.emit("init"); // 输出: 初始化
ee.emit("init"); // 没输出(监听器已被移除)
ee.emit("init"); // 没输出
// off / removeListener:手动移除监听器
function onMsg(msg) { console.log("收到:", msg); }
ee.on("message", onMsg);
ee.emit("message", "hi"); // 输出
ee.off("message", onMsg); // 移除
ee.emit("message", "again"); // 没输出
// removeAllListeners:移除某事件的所有监听器
ee.removeAllListeners("message");几个常用 API 速查:
on(event, fn):注册监听器(可多次触发)。once(event, fn):只触发一次。off(event, fn)/removeListener:移除指定监听器(必须传同一个函数引用,匿名函数移除不了)。removeAllListeners(event):移除某事件全部监听器。listeners(event):返回该事件的所有监听器数组。listenerCount(event):返回监听器数量。
3. 实战:让自定义类支持事件
实际项目里最常见的设计是让一个类继承 EventEmitter,在业务关键点触发事件,让外部代码灵活订阅。这是一种解耦的好手段——核心类不用关心"事件被谁怎么处理":
// 实际项目里,通常让自定义类继承 EventEmitter
const EventEmitter = require("events");
class UserService extends EventEmitter {
constructor() {
super();
this.users = [];
}
addUser(name) {
this.users.push(name);
// 业务逻辑里触发事件,通知外部
this.emit("userAdded", name);
}
removeUser(name) {
this.users = this.users.filter(u => u !== name);
this.emit("userRemoved", name);
}
}
// 使用
const service = new UserService();
// 外部监听(解耦:UserService 不用知道具体怎么处理)
service.on("userAdded", (name) => {
console.log("[日志] 新用户:", name);
});
service.on("userAdded", (name) => {
// 这里可以发邮件、写数据库、推消息...
sendEmail(name);
});
service.addUser("小明");
// [日志] 新用户: 小明
// (发邮件...)这种模式的威力在于解耦:UserService 只管"用户被添加了,通知一声",至于具体怎么响应(写日志、发邮件、推消息、更新缓存),完全由调用方决定。可以随时加新的监听器,核心代码一行都不用改。
4. error 事件:必须处理
EventEmitter 有一个特殊的事件名 "error"。它的特殊之处在于:如果 emit 一个 error 事件却没有监听器,Node 进程会直接崩溃。这是 EventEmitter 唯一的"硬性"约定:
// EventEmitter 有个特殊事件名:"error"
// 如果 emit("error") 时没有监听器,进程会直接崩溃!
const ee = new EventEmitter();
ee.emit("error", new Error("出错了"));
// ↑ 没注册 error 监听 → 整个 Node 进程崩溃,抛出栈
// 正确做法:总是监听 error 事件
ee.on("error", (err) => {
console.error("捕获到错误:", err.message);
});
ee.emit("error", new Error("又一个错"));
// 输出: 捕获到错误: 又一个错(进程不崩)所以写自定义事件发射器时,error 事件一定要有兜底监听,否则线上一个未捕获的错误就让服务挂掉。
5. 元事件与内存泄漏保护
EventEmitter 还提供了"元事件"——监听"监听器本身"的增减:
const ee = new EventEmitter();
// newListener 事件:每当有新监听器注册时触发
ee.on("newListener", (event, listener) => {
console.log("新监听器:", event);
});
// removeListener 事件:监听器被移除时触发
ee.on("removeListener", (event, listener) => {
console.log("监听器移除:", event);
});
// 监听器数量
ee.on("x", () => {});
ee.on("x", () => {});
console.log(ee.listenerCount("x")); // 2
console.log(ee.listeners("x").length); // 2
// 默认最大监听器数 10,超过会警告(可能是内存泄漏)
// 可以调高(但通常是代码设计问题,先排查)
ee.setMaxListeners(20);内存泄漏警告:Node 默认限制每个事件最多 10 个监听器。超过会打印警告——这通常是 bug 的信号(比如在循环里反复 on 却忘了 off)。看到这个警告要警惕,不要简单 setMaxListeners(0) 关掉了事,先排查是不是真的忘了移除监听器。
6. 同步还是异步?
这是最容易误解的点:emit 是同步的。它会在当前执行的代码里,依次调用所有监听器,等它们全部返回后才继续往下。看这个例子:
const ee = new EventEmitter();
ee.on("x", () => console.log("监听器 1"));
ee.on("x", () => console.log("监听器 2"));
console.log("before");
ee.emit("x"); // 同步触发
console.log("after");
// 输出顺序:
// before
// 监听器 1
// 监听器 2
// after但 Node 里的"异步事件"——比如 stream 的 data 事件、fs.readFile 完成后的回调——是事件循环在下个 tick 触发的,所以感觉是异步的。区分清楚:"emit 本身同步,但什么时候 emit 由你的代码决定"。
7. 真实案例:stream 都基于 EventEmitter
Node 里最常用事件的地方就是流(Stream)。流是一个 EventEmitter,触发 data、end、error 等事件:
const fs = require("fs");
// 读流:每读到一块数据触发 data
const stream = fs.createReadStream("big.txt");
stream.on("data", (chunk) => {
console.log("读到", chunk.length, "字节");
});
stream.on("end", () => {
console.log("读完了");
});
stream.on("error", (err) => {
console.error("出错:", err);
});HTTP 服务的 req 和 res 也是流(继承自 EventEmitter)。所以你在前面 HTTP 章节看到的 req.on("data", ...),本质上就是 EventEmitter 的用法。下一篇我们就专门讲文件系统。
小结
EventEmitter 是 Node 事件驱动的核心。记住四点:on 注册、emit 触发(同步)、once 一次性、error 必须监听。它不只是内置 API,更是一种解耦的设计模式——在业务关键点触发事件,让外部灵活订阅。后面学 stream、http、甚至自己设计类库,都会反复用到这套思路。
← 上一篇 Node.js HTTP 服务
下一篇 Node.js 文件系统 →