Node.js 事件循环机制
事件循环概述
Node.js 的事件循环是其非阻塞 I/O 模型的核心。尽管 JavaScript 是单线程的,事件循环使得 Node.js 可以高效处理数千并发连接。
事件循环的阶段
事件循环按顺序执行以下阶段(phase):
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ close callbacks │
│ └───────────────────────────┘
1. timers 阶段
执行 setTimeout() 和 setInterval() 的回调。计时器指定的是最小延迟时间,而非精确执行时间。
setTimeout(() => {
console.log('Timer callback');
}, 0);
2. pending callbacks 阶段
执行推迟到下一轮循环的 I/O 回调,如某些系统错误回调。
3. idle, prepare 阶段
内部使用。
4. poll 阶段
事件循环的核心阶段。主要做两件事:
- 计算阻塞时间
- 处理 I/O 事件队列中的回调
如果 poll 队列不为空,同步执行回调直到队列清空或达到硬限制。如果队列为空,则检查是否有 setImmediate() 或已到期的计时器。
5. check 阶段
执行 setImmediate() 的回调。它在 poll 阶段空闲后立即执行:
setImmediate(() => {
console.log('Immediate callback');
});
6. close callbacks 阶段
执行关闭事件的回调,如 socket.on('close', ...)。
微任务与宏任务
在每次切换阶段时,事件循环会处理微任务队列。微任务的优先级高于宏任务(即事件循环阶段的回调)。
微任务
process.nextTick()(优先级最高)- Promise 的
.then()/.catch()/.finally()
宏任务(即事件循环各阶段的回调)
setTimeout/setIntervalsetImmediate- I/O 回调
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
process.nextTick(() => console.log('4'));
console.log('5');
// 输出: 1, 5, 4, 3, 2
process.nextTick 与 setImmediate
process.nextTick()在当前操作完成后立即执行,属于微任务setImmediate()在事件循环的下一个 check 阶段执行
虽然 process.nextTick() 语义更直观,但过度使用会导致 I/O 饥饿。推荐优先使用 setImmediate()。
常见误区
误区 1:setTimeout(fn, 0) 立即执行
setTimeout(fn, 0) 至少有 1ms 的延迟。在事件循环中,它在 timers 阶段执行,晚于同步代码和微任务。
误区 2:事件循环是单线程的
JavaScript 回调在主线程执行,但 Node.js 内部(libuv)使用线程池处理文件 I/O、DNS 查询等操作。
误区 3:所有异步操作在同一个阶段完成
不同类型的异步操作在事件循环的不同阶段处理,理解各阶段的顺序才能准确预测代码执行顺序。