第 7 章 · 事件循环
第 7 章 · 事件循环
7.1 事件循环概述
事件循环(Event Loop)是 Node.js 的核心机制,它让单线程的 JavaScript 能够处理并发操作。
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout / setInterval
│ └──────────┬────────────────┘
│ ┌──────────┴────────────────┐
│ │ pending callbacks │ ← 系统级回调(TCP 错误等)
│ └──────────┬────────────────┘
│ ┌──────────┴────────────────┐
│ │ idle, prepare │ ← 内部使用
│ └──────────┬────────────────┘
│ ┌──────────┴────────────────┐
│ │ poll │ ← I/O 操作(文件、网络)
│ └──────────┬────────────────┘
│ ┌──────────┴────────────────┐
│ │ check │ ← setImmediate
│ └──────────┬────────────────┘
│ ┌──────────┴────────────────┐
│ │ close callbacks │ ← socket.on('close')
│ └──────────┬────────────────┘
│ │
└─────────────┘
7.2 六个阶段详解
Timers 阶段
执行 setTimeout 和 setInterval 的回调。
setTimeout(() => {
console.log('setTimeout');
}, 0);
setInterval(() => {
console.log('setInterval');
}, 1000);
Pending Callbacks 阶段
执行被推迟到下一次循环的系统级回调,如 TCP 错误。
Poll 阶段
这是最重要的阶段,负责:
- 执行 I/O 回调(文件读写、网络请求等)
- 计算应该阻塞多久等待 I/O
poll 阶段行为:
1. 如果有 I/O 回调 → 执行回调(直到队列为空或达到系统上限)
2. 如果没有回调 →
a. 如果有 setImmediate → 结束 poll,进入 check
b. 如果没有 setImmediate → 等待 I/O(有超时时间)
Check 阶段
执行 setImmediate 的回调。
Close Callbacks 阶段
执行关闭事件的回调,如 socket.on('close')。
7.3 process.nextTick 与微任务
优先级对比
setTimeout(() => console.log('1. setTimeout'), 0);
setImmediate(() => console.log('2. setImmediate'));
Promise.resolve().then(() => console.log('3. Promise'));
process.nextTick(() => console.log('4. process.nextTick'));
queueMicrotask(() => console.log('5. queueMicrotask'));
console.log('6. 同步代码');
// 输出顺序:
// 6. 同步代码
// 4. process.nextTick ← 最高优先级
// 5. queueMicrotask
// 3. Promise
// 1. setTimeout 或 2. setImmediate(顺序不确定)
微任务队列
宏任务队列(事件循环各阶段):
timers → pending → poll → check → close
微任务队列(每个阶段之间清空):
process.nextTick → Promise.then → queueMicrotask
执行顺序:
1. 执行当前宏任务
2. 清空所有微任务(nextTick 优先于 Promise)
3. 进入下一个宏任务阶段
// 微任务会在每个阶段之间执行
const fs = require('fs');
fs.readFile(__filename, () => {
// I/O 回调(poll 阶段)
console.log('1. I/O 回调');
setTimeout(() => console.log('2. setTimeout(timers)'), 0);
setImmediate(() => console.log('3. setImmediate(check)'));
process.nextTick(() => console.log('4. nextTick'));
Promise.resolve().then(() => console.log('5. Promise'));
});
// 输出:
// 1. I/O 回调
// 4. nextTick
// 5. Promise
// 3. setImmediate(check 在 timers 之前)
// 2. setTimeout
7.4 process.nextTick 的危险
// ❌ 递归 nextTick 会阻塞事件循环
function dangerous() {
process.nextTick(dangerous); // 永远不会让出控制权
}
// dangerous(); // 不要这样做!
// ✅ 使用 setImmediate 让出控制权
function cooperative() {
setImmediate(cooperative); // 每次迭代让出控制权
}
// 对比:递归 nextTick vs setImmediate
process.nextTick(() => console.log('nextTick 1'));
process.nextTick(() => console.log('nextTick 2'));
setImmediate(() => console.log('immediate 1'));
setImmediate(() => console.log('immediate 2'));
// nextTick 1
// nextTick 2
// immediate 1
// immediate 2
7.5 实战:理解事件循环顺序
// test-event-loop.js
const fs = require('fs');
console.log('1. 同步: 开始');
// Timer
setTimeout(() => {
console.log('5. Timer: setTimeout');
}, 0);
// Immediate
setImmediate(() => {
console.log('6. Check: setImmediate');
});
// I/O
fs.readFile(__filename, () => {
console.log('7. Poll: I/O 完成');
setTimeout(() => console.log('8. Timer: I/O 内 setTimeout'), 0);
setImmediate(() => console.log('9. Check: I/O 内 setImmediate'));
});
// NextTick
process.nextTick(() => {
console.log('3. 微任务: process.nextTick');
});
// Microtask
Promise.resolve().then(() => {
console.log('4. 微任务: Promise.then');
});
console.log('2. 同步: 结束');
// 输出:
// 1. 同步: 开始
// 2. 同步: 结束
// 3. 微任务: process.nextTick
// 4. 微任务: Promise.then
// 5. Timer: setTimeout
// 6. Check: setImmediate
// 7. Poll: I/O 完成
// 8. Timer: I/O 内 setTimeout
// 9. Check: I/O 内 setImmediate
7.6 定时器精度
// setTimeout 的最小延迟
const start = Date.now();
setTimeout(() => {
console.log(`实际延迟: ${Date.now() - start}ms`);
}, 1);
// 实际延迟通常为 1-2ms,但系统负载高时可能更大
// setTimeout(fn, 0) 不等于立即执行
setTimeout(() => console.log('A'), 0);
setImmediate(() => console.log('B'));
// 顺序不确定!取决于进入事件循环的时间
// 纳秒级计时
const { performance, PerformanceObserver } = require('perf_hooks');
const obs = new PerformanceObserver((items) => {
items.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration.toFixed(3)}ms`);
});
});
obs.observe({ entryTypes: ['measure'] });
performance.mark('start');
setTimeout(() => {
performance.mark('end');
performance.measure('setTimeout', 'start', 'end');
}, 0);
注意事项
⚠️ process.nextTick 不是 setTimeout 的替代品:递归调用 nextTick 会阻止 I/O 和其他回调执行,导致"饥饿"问题。
⚠️ setTimeout(fn, 0) 实际延迟大于 0:Node.js 内部最小延迟为 1ms,系统负载高时可能更大。
⚠️ 在 I/O 循环内 setImmediate 总是先于 setTimeout:这是唯一可以确定两者顺序的场景。
扩展阅读
上一章:第 6 章 · 异步编程基础 下一章:第 8 章 · 流(Streams) — 可读、可写、Transform、Duplex 和管道。