Node 事件驱动深入
Node 的一切异步行为,最终都能归结为事件驱动:注册回调、等待触发。浏览器里的事件是用户操作(点击、滚动),Node 里的事件则是"数据到了""连接来了""文件读完了"。理解事件系统与事件循环,就掌握了 Node 运行时的核心。
一、EventEmitter 概念
events 模块提供 EventEmitter 类,是 Node 所有事件能力的基础。fs、http、stream 都继承自它:
javascript
const EventEmitter = require("events");
const emitter = new EventEmitter();
// 注册监听器
emitter.on("greet", (name) => console.log(`你好,${name}`));
emitter.once("greet", (name) => console.log(`(只执行一次)欢迎,${name}`));
// 触发事件
emitter.emit("greet", "小明");
emitter.emit("greet", "小红");
// 输出:
// 你好,小明
// (只执行一次)欢迎,小明
// 你好,小红 ← once 的监听器已移除,不再触发1.1 核心方法速查
| 方法 | 作用 |
|---|---|
on(event, fn) | 注册监听器,可重复触发 |
once(event, fn) | 注册一次性监听器,触发后自动移除 |
emit(event, ...args) | 触发事件,参数传给监听器 |
off(event, fn) | 移除指定监听器(removeListener 的别名) |
removeAllListeners([event]) | 移除全部(或指定事件)的监听器 |
listenerCount(event) | 某事件的监听器数量 |
1.2 事件名约定
- 事件名用字符串,惯例是小驼峰或短横线命名:
data、dataReceived、connection - 表示"发生某件事"的名词性事件名:
data、close、error - 预定义 Node 事件(如
error)不要随意覆盖语义
二、自定义 EventEmitter 类
实际开发中,通常让业务类继承 EventEmitter:
javascript
const { EventEmitter } = require("events");
class OrderService extends EventEmitter {
constructor() {
super();
this.orders = [];
}
createOrder(order) {
this.orders.push(order);
this.emit("orderCreated", order); // 通知所有监听者
if (order.total > 1000) {
this.emit("bigOrder", order); // 大额订单额外事件
}
}
}
const service = new OrderService();
service.on("orderCreated", (order) => {
console.log("新订单:", order.id);
});
service.on("bigOrder", (order) => {
console.log("大额订单,需要人工审核:", order.total);
});
service.createOrder({ id: 1, total: 500 }); // 新订单:1
service.createOrder({ id: 2, total: 5000 }); // 新订单:2 + 大额订单注意:多个回调共享一个事件对象时,回调里的
this会指向 EventEmitter 实例,用箭头函数或显式绑定可以规避歧义。
三、error 事件的特殊处理
error 是 EventEmitter 的特殊事件:
emit("error", err)时如果没有监听器,Node 会直接抛出该错误,导致进程崩溃- 因此,能产生错误的事件源必须注册
error监听
javascript
const { EventEmitter } = require("events");
const emitter = new EventEmitter();
// 危险:触发 error 但没有监听器 → 进程直接崩溃
// emitter.emit("error", new Error("boom!"));
// 正确:先注册 error 监听
emitter.on("error", (err) => {
console.error("错误被捕获:", err.message);
});
emitter.emit("error", new Error("boom!")); // 正常输出,不崩溃javascript
// 实际案例:流、服务器都有 error 事件,必须监听
const fs = require("fs");
fs.createReadStream("missing.txt")
.on("error", (err) => console.error("文件读取错误:", err.code));| 情况 | 后果 |
|---|---|
触发 error 且无监听器 | 错误被抛出,进程崩溃 |
触发 error 且有监听器 | 监听器处理,进程存活 |
| 其他事件无监听器 | 静默忽略,无任何影响 |
四、事件监听器管理
javascript
const { EventEmitter } = require("events");
const emitter = new EventEmitter();
const fn1 = () => console.log("监听器1");
const fn2 = () => console.log("监听器2");
emitter.on("data", fn1);
emitter.on("data", fn2);
console.log(emitter.listenerCount("data")); // 2
emitter.off("data", fn1); // 移除 fn1
console.log(emitter.listenerCount("data")); // 1
emitter.removeAllListeners("data"); // 清空 data 事件
console.log(emitter.listenerCount("data")); // 04.1 maxListeners 警告
同一事件注册超过 10 个监听器时,Node 会打印警告(默认):
javascript
const { EventEmitter } = require("events");
const emitter = new EventEmitter();
emitter.setMaxListeners(50); // 调高上限,消除警告
console.log(emitter.getMaxListeners()); // 50| API | 用途 |
|---|---|
listenerCount(event) | 统计监听器数量 |
removeAllListeners(event?) | 批量移除 |
setMaxListeners(n) | 修改单事件监听器上限 |
getMaxListeners() | 读取当前上限 |
emitter.rawListeners(event) | 获取监听器数组(含包装函数) |
警告通常是"内存泄漏"的信号:如果一个事件被反复添加监听器又没移除,事件对象会越积越多。排查时先看 listenerCount 是否持续增长。
五、事件循环:libuv 各阶段
Node 的事件循环由 libuv 实现,一个循环由多个阶段组成,每个阶段有自己的任务队列:
text
┌───────────────────────────┐
┌─►│ timers(定时器回调) │
│ ├───────────────────────────┤
│ │ pending callbacks │ ← 上一轮遗留的 I/O 回调
│ ├───────────────────────────┤
│ │ idle / prepare(内部) │
│ ├───────────────────────────┤
│ │ poll(轮询 I/O) │ ← 停留最久,等文件/网络事件
│ ├───────────────────────────┤
│ │ check(setImmediate) │
│ ├───────────────────────────┤
│ │ close callbacks │ ← socket 等资源的关闭回调
│ └───────────────────────────┘
└──────── 回到 timers 开始下一轮| 阶段 | 处理内容 |
|---|---|
| timers | setTimeout、setInterval 到期的回调 |
| pending callbacks | 推迟到下一轮的 I/O 回调(如部分系统错误) |
| idle / prepare | libuv 内部使用 |
| poll | 等待并处理 I/O 事件(文件、网络、信号) |
| check | setImmediate 的回调 |
| close callbacks | close 事件(如 socket 关闭) |
六、process.nextTick 与微任务
重点:process.nextTick 的队列不属于事件循环的阶段,它在每个阶段切换的间隙执行——即"当前 JS 代码执行完后、进入下一个阶段前"立刻执行。
javascript
console.log("1 同步代码");
process.nextTick(() => console.log("2 nextTick"));
Promise.resolve().then(() => console.log("3 Promise 微任务"));
setTimeout(() => console.log("4 setTimeout"), 0);
// 输出顺序:1 → 2 → 3 → 4| 对比项 | process.nextTick | Promise.then(微任务) |
|---|---|---|
| 优先级 | 更高,先于 Promise 微任务 | 次之 |
| 所属队列 | nextTick 队列(不在事件循环阶段内) | 微任务队列 |
| 适用场景 | 尽快执行、API 内部保证时序 | 常规异步处理 |
陷阱:在
nextTick回调里不断注册新的nextTick,会让 poll 阶段永远无法进入(饿死 I/O)。标准建议是优先用微任务或setImmediate。
javascript
// 区分 nextTick 与 setImmediate
process.nextTick(() => console.log("nextTick 优先"));
setImmediate(() => console.log("setImmediate 次之"));
// 输出:nextTick 优先 → setImmediate 次之七、setImmediate 与 setTimeout
javascript
// 在模块(非 I/O 回调)中:
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
// 顺序不确定!两者都在脚本执行后触发,先后受启动耗时影响
// 在 I/O 回调中(如 fs.readFile 的 callback):
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("setTimeout"));
setImmediate(() => console.log("setImmediate"));
});
// 稳定输出:setImmediate 先于 setTimeout
// 因为 poll 阶段结束后先进入 check 阶段,timers 要等下一轮| 对比项 | setTimeout(fn, 0) | setImmediate(fn) |
|---|---|---|
| 执行阶段 | timers 阶段 | check 阶段 |
| 与 I/O 回调的相对顺序 | I/O 之后下一轮 | 紧跟本轮 poll 之后 |
| 推荐场景 | 常规延时 | "异步但尽快执行"(优先于 setTimeout) |
八、事件循环执行顺序示例
综合示例,一次看清各任务的执行次序:
javascript
const fs = require("fs");
console.log("1 同步代码");
setTimeout(() => console.log("2 setTimeout"), 0);
setImmediate(() => console.log("3 setImmediate"));
process.nextTick(() => console.log("4 nextTick"));
Promise.resolve().then(() => console.log("5 Promise"));
fs.readFile(__filename, () => {
console.log("6 I/O 回调(poll 阶段)");
process.nextTick(() => console.log("7 I/O 内 nextTick"));
setImmediate(() => console.log("8 I/O 内 setImmediate"));
setTimeout(() => console.log("9 I/O 内 setTimeout"), 0);
});
// 输出顺序:
// 1 同步代码
// 4 nextTick ← nextTick 在当前 JS 代码结束后立即执行
// 5 Promise ← 微任务
// 6 I/O 回调 ← 文件读完后在 poll 阶段执行
// 7 I/O 内 nextTick ← I/O 回调执行完立即执行
// 8 I/O 内 setImmediate ← 本轮 check 阶段
// 9 I/O 内 setTimeout ← 下一轮 timers 阶段
// (2、3 的顺序在 I/O 外不稳定,取决于启动时序)8.1 记忆要点
| 规则 | 说明 |
|---|---|
| 同步代码永远最先执行 | 事件循环只有在调用栈清空后才开始 |
| nextTick 优先于微任务 | nextTick 队列在每次切换阶段前清空 |
| 微任务优先于阶段回调 | 每个阶段之间都会先清空微任务 |
| I/O 回调后,setImmediate 优先于 setTimeout | 先 check 后 timers |
九、事件驱动架构的优势与局限
| 维度 | 优势 | 局限 |
|---|---|---|
| 并发能力 | 单进程扛大量 I/O 并发,资源开销小 | CPU 密集任务会阻塞事件循环 |
| 代码模型 | 回调/事件解耦,扩展新行为只需加监听 | 复杂业务易陷入"回调地狱" |
| 内存 | 无需为每个连接开线程 | 事件泄漏(忘移除监听)会涨内存 |
| 一致性 | 同一时刻只有一段 JS 在跑,无数据竞争 | 单线程算力上限,需 cluster/worker_threads 补强 |
javascript
// 错误示范:CPU 密集计算卡死事件循环
const http = require("http");
http.createServer((req, res) => {
// 同步计算大斐波那契数:占用主线程,所有请求全部卡住
const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2));
res.end(String(fib(40)));
}).listen(3000);
// 正确姿势:把计算交给 worker_threads,或拆成小步异步执行| 优化方向 | 手段 |
|---|---|
| 拆分 CPU 任务 | 移到 worker_threads |
| 提升吞吐 | cluster 多进程 |
| 防止事件泄漏 | 及时 off、用 once |
| 控制并发 | 限制同一事件的监听数量与触发频率 |
事件驱动是 Node 的灵魂:EventEmitter 提供编程模型,libuv 事件循环提供执行引擎,二者配合实现了"小资源、大并发"。写异步代码时心里始终装着阶段顺序与 nextTick,就能准确预测执行时序。