Node.js 事件循环机制深度解析
事件循环决定了异步任务的执行顺序,深入理解它对性能优化和问题排查至关重要。
事件循环架构
Node.js 事件循环基于 libuv 实现,核心结构:
JavaScript
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─>│ pending callbacks │
│ │ └───────┬─────────────┘
│ │ ┌─>│ idle, prepare │
│ │ │ └───────┬─────────────┘
│ │ │ ┌─>│ poll │<─── I/O 事件
│ │ │ │ └───────┬─────────────┘
│ │ │ │ ┌─>│ check │
│ │ │ │ │ └───────┬─────────────┘
│ │ │ │ │ ┌─>│ close callbacks │
│ │ │ │ │ │ └───────┬─────────────┘
│ │ │ │ │ │ └───────┘
│ │ │ │ │ └─> 退出循环
六个阶段详解
timers 阶段
执行 setTimeout/setInterval 到期的回调:
JavaScript
// timers 阶段执行已到期定时器
setTimeout(() => console.log('timer'), 100);
// 定时器到期时间检查
// 在 poll 阶段计算是否有到期定时器
定时器执行时机不精确,poll 阶段阻塞时可能延迟执行。
pending callbacks 阶段
执行系统操作回调(上一轮循环未执行的 I/O 回调):
JavaScript
// TCP 错误处理
socket.on('error', (err) => {
// 在 pending callbacks 阶段执行
});
idle, prepare 阶段
libuv 内部使用,无需关注。
poll 阶段
核心阶段,处理 I/O 事件:
JavaScript
// poll 阶段行为
// 1. 计算最长等待时间
// 2. 执行 I/O 回调
// 3. 检查是否有到期定时器
// poll 阶段阻塞规则
// - 有 I/O 回调:执行后进入 check
// - 无 I/O 回调:
// - 有 setImmediate:进入 check
// - 有到期定时器:进入 timers
// - 否则:等待新 I/O 事件
check 阶段
执行 setImmediate 回调:
JavaScript
setImmediate(() => {
// 在 check 阶段执行
});
close callbacks 阶段
执行 close 事件回调:
JavaScript
socket.destroy();
socket.on('close', () => {
// 在 close callbacks 阶段执行
});
阶段切换机制
JavaScript
// poll 阶段决策流程
if (hasIOCallbacks) {
executeCallbacks();
goto check;
} else if (hasSetImmediate) {
goto check;
} else if (hasExpiredTimers) {
goto timers;
} else {
waitForIO(timeout);
}
timers 与 setImmediate 执行顺序
JavaScript
// 在非 I/O 回调中(不确定顺序)
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 顺序可能不同,取决于进程启动时机
// 在 I/O 回调中(确定顺序)
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 输出: immediate -> timeout
// I/O 回调在 poll 阶段,下一阶段是 check
循环退出条件
JavaScript
// 事件循环退出的条件
// 1. 没有 active 定时器
// 2. 没有 active的 I/O 操作
// 3. 没有 setImmediate 待执行
// process._getActiveRequests()
// process._getActiveHandles()
事件循环监控
JavaScript
// 监控循环延迟
const { performance } = require('perf_hooks');
const timerify = performance.timerify;
function measure() {
const start = performance.now();
setImmediate(() => {
const lag = performance.now() - start;
console.log('循环延迟:', lag);
});
}
measure();
UV_THREADPOOL_SIZE
JavaScript
// libuv 线程池大小(默认 4)
process.env.UV_THREADPOOL_SIZE = 8;
// 影响:fs、crypto、zlib 等异步操作并发度
实例分析
text
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
setImmediate(() => {
console.log('4');
Promise.resolve().then(() => console.log('5'));
});
Promise.resolve()
.then(() => console.log('6'))
.then(() => console.log('7'));
process.nextTick(() => console.log('8'));
console.log('9');
// 输出顺序分析:
// 1, 9 (同步)
// 8 (nextTick)
// 6, 7 (微任务)
// 2 或 4 (取决于 timers/poll)
// 3 或 5 (微任务)
// ...
要点总结
- 事件循环分六阶段:timers → pending → idle → poll → check → close
- poll 是核心阶段,处理 I/O 并决定下一步
- timers 执行时机不精确,poll 阻塞会延迟
- I/O 回调中 setImmediate 优先于 setTimeout
- UV_THREADPOOL_SIZE 影响异步 I/O 并发度
📝 发现内容有误?点击此处直接编辑