Node.js: 异步编程:事件循环
最后更新:2026-08-26
1. 故事:被 200ms 拦住的服务
Bob 的 API 服务平时响应只要 5ms,但高峰期飙到 2000ms。用户抱怨不断,他一开始以为是数据库慢,加了索引却没改善。后来用 console.time 逐段排查,发现一个同步的 JSON 大数组排序耗时 200ms——这 200ms 事件循环被卡死,所有排队请求都在等它完成。理解事件循环后,Bob 用 setImmediate 把排序拆成小片轮流执行,高峰期响应降到 50ms。
你将学到:
- 事件循环 6 个阶段的职责与流转
- 微任务队列(process.nextTick / Promise.then)的优先级
- 宏任务队列(setTimeout / setImmediate / I/O)的调度
- setTimeout vs setImmediate vs process.nextTick 的执行顺序
- I/O 上下文中 setImmediate 先于 setTimeout 的原因
- 阻塞事件循环的危害与拆分策略
2. 事件循环的 6 个阶段
Node.js 事件循环由 libuv 驱动,依次经过 6 个阶段,每轮循环从 timers 开始,到 close callbacks 结束,然后回到 timers 开始下一轮。
flowchart TD
A["timers<br/>setTimeout / setInterval"] --> B["pending callbacks<br/>系统级回调"]
B --> C["idle, prepare<br/>内部使用"]
C --> D["poll<br/>I/O 回调 / 新 I/O 检索"]
D --> E["check<br/>setImmediate"]
E --> F["close callbacks<br/>socket.on('close')"]
F --> A
| 阶段 | 回调类型 | 说明 |
|---|---|---|
| timers | setTimeout / setInterval | 执行到期的定时器回调,受最小 1ms 延迟限制 |
| pending callbacks | 系统级回调 | 推迟到下一轮循环执行的 I/O 回调(如 TCP 错误) |
| idle, prepare | 内部使用 | libuv 内部使用,开发者一般不接触 |
| poll | I/O 回调 | 检索新 I/O 事件,执行 I/O 相关回调;若无定时器,会在此阶段阻塞等待 |
| check | setImmediate | setImmediate 回调在此阶段执行 |
| close callbacks | close 事件 | 如 socket.on('close') 等关闭回调 |
3. 微任务与宏任务
(1) (3.1) 微任务队列(Microtask)
微任务在当前阶段结束后、下一阶段开始前全部执行。微任务队列有两个优先级:
| 微任务 | 优先级 | 说明 |
|---|---|---|
| process.nextTick | 最高 | 每个阶段结束后优先清空 nextTick 队列 |
| Promise.then / catch / finally | 次高 | nextTick 队列清空后执行 |
▶ 示例:nextTick 优先于 Promise.then
JAVASCRIPT
process.nextTick(() => {
console.log('nextTick 1');
});
Promise.resolve().then(() => {
console.log('promise.then 1');
});
process.nextTick(() => {
console.log('nextTick 2');
});
Promise.resolve().then(() => {
console.log('promise.then 2');
});
TEXT
📖 仅展示
nextTick 1
nextTick 2
promise.then 1
promise.then 2
(2) (3.2) 宏任务队列(Macrotask)
宏任务由事件循环各阶段调度,每阶段执行对应的宏任务。
| 宏任务 | 所属阶段 | 说明 |
|---|---|---|
| setTimeout / setInterval | timers | 最小延迟 1ms,到期后进入 timers 队列 |
| setImmediate | check | 下轮循环 check 阶段执行 |
| I/O 回调 | poll | 文件读写、网络等 I/O 完成回调 |
| close callbacks | close callbacks | 关闭事件回调 |
4. setTimeout vs setImmediate vs process.nextTick
▶ 示例:外层上下文执行顺序不确定
JAVASCRIPT
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
在非 I/O 上下文中,setTimeout 和 setImmediate 的执行顺序不确定——取决于系统进程调度,可能先输出任何一个。
▶ 示例:I/O 上下文中 setImmediate 必定先执行
JAVASCRIPT
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
TEXT
📖 仅展示
setImmediate
setTimeout
I/O 回调在 poll 阶段执行,结束后进入 check 阶段(setImmediate),然后才到下一轮 timers 阶段(setTimeout)。
▶ 示例:process.nextTick 始终最先
JAVASCRIPT
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
process.nextTick(() => {
console.log('nextTick');
});
TEXT
📖 仅展示
nextTick
setTimeout (或 setImmediate,取决于调度)
setImmediate
| 特性 | setTimeout(fn, 0) | setImmediate | process.nextTick |
|---|---|---|---|
| 所属队列 | 宏任务(timers) | 宏任务(check) | 微任务(最高优先) |
| 执行阶段 | timers | check | 当前阶段结束后立即 |
| 最小延迟 | 1ms | 无 | 无 |
| I/O 上下文顺序 | 后于 setImmediate | 先于 setTimeout | 最先 |
| 外层上下文顺序 | 不确定 | 不确定 | 最先 |
5. 微任务 vs 宏任务对比
| 维度 | 微任务 | 宏任务 |
|---|---|---|
| 代表 | process.nextTick、Promise.then | setTimeout、setImmediate、I/O 回调 |
| 执行时机 | 当前阶段结束后、下一阶段前 | 事件循环对应阶段中 |
| 执行策略 | 一次性全部清空 | 每阶段执行一个后检查微任务 |
| 嵌套产生 | 执行中产生的微任务也在本轮清空 | 推到后续阶段执行 |
| 优先级 | 高于所有宏任务 | 低于微任务 |
▶ 示例:微任务穿插在宏任务之间
JAVASCRIPT
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => {
console.log('promise between 1 and 2');
});
}, 0);
setTimeout(() => {
console.log('timeout 2');
}, 0);
TEXT
📖 仅展示
timeout 1
promise between 1 and 2
timeout 2
每个宏任务执行完后,都会清空微任务队列,再执行下一个宏任务。
6. 阻塞事件循环的危害与避免
▶ 示例:阻塞事件循环
JAVASCRIPT
const http = require('http');
const server = http.createServer((req, res) => {
const start = Date.now();
while (Date.now() - start < 200) {}
res.end('done');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
每个请求都会卡住事件循环 200ms,期间所有其他请求都无法处理。
▶ 示例:用 setImmediate 拆分长任务
JAVASCRIPT
function chunkedSort(arr, compareFn, callback) {
const CHUNK = 1000;
let offset = 0;
function sortChunk() {
const end = Math.min(offset + CHUNK, arr.length);
for (let i = offset; i < end; i++) {
for (let j = i + 1; j < end; j++) {
if (compareFn(arr[i], arr[j]) > 0) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
offset = end;
if (offset < arr.length) {
setImmediate(sortChunk);
} else {
callback(arr);
}
}
setImmediate(sortChunk);
}
chunkedSort(
[5, 3, 8, 1, 9, 2, 7, 4, 6],
(a, b) => a - b,
(result) => {
console.log('Sorted:', result);
}
);
| 维度 | 阻塞代码 | 非阻塞代码 |
|---|---|---|
| 计算方式 | 同步循环一次跑完 | 拆分成小片,用 setImmediate 轮流执行 |
| 事件循环 | 卡死,其他请求等待 | 每片之间让出控制权,其他回调可执行 |
| 响应时间 | 累积延迟 | 分散延迟,用户可接受 |
| CPU 利用 | 单核 100% 突发 | 分散使用,留出 I/O 时间 |
| 适用场景 | 极短计算 | 长计算、高并发服务 |
7. 综合示例:事件循环执行顺序验证脚本
以下脚本验证 setTimeout → process.nextTick → setImmediate → Promise.then → I/O callback 的完整执行顺序。
JAVASCRIPT
const fs = require('fs');
console.log('--- Script start ---');
setTimeout(() => {
console.log('1. setTimeout (macrotask - timers)');
}, 0);
setImmediate(() => {
console.log('2. setImmediate (macrotask - check)');
});
process.nextTick(() => {
console.log('3. process.nextTick (microtask - highest)');
});
Promise.resolve().then(() => {
console.log('4. Promise.then (microtask)');
});
fs.readFile(__filename, () => {
console.log('5. I/O callback (poll phase)');
setTimeout(() => {
console.log('6. inner setTimeout');
}, 0);
setImmediate(() => {
console.log('7. inner setImmediate');
});
process.nextTick(() => {
console.log('8. inner nextTick');
});
Promise.resolve().then(() => {
console.log('9. inner Promise.then');
});
});
console.log('--- Script end ---');
TEXT
📖 仅展示
--- Script start ---
--- Script end ---
3. process.nextTick (microtask - highest)
4. Promise.then (microtask)
1. setTimeout (macrotask - timers)
2. setImmediate (macrotask - check)
5. I/O callback (poll phase)
8. inner nextTick
9. inner Promise.then
7. inner setImmediate
6. inner setTimeout
执行顺序解析:
- 同步代码先执行,输出 Script start / Script end
- 当前阶段结束后清空微任务:nextTick → Promise.then
- 进入 timers 阶段:setTimeout
- 进入 check 阶段:setImmediate
- I/O 完成后在 poll 阶段执行回调
- I/O 回调结束后清空微任务:inner nextTick → inner Promise.then
- poll 结束进入 check:inner setImmediate
- 下一轮 timers:inner setTimeout
❓ 常见问题
Q setTimeout(fn, 0) 是真的 0ms 后执行吗?
A 不是。浏览器和 Node.js 都有最小延迟(约 1ms),且必须等到事件循环进入 timers 阶段才会执行,实际等待时间受当前阶段影响。
Q process.nextTick 和 Promise.then 谁先执行?
A process.nextTick 先执行。微任务队列中 nextTick 优先级最高,每次都会先清空 nextTick 队列再清空 Promise 队列。
Q 为什么 I/O 回调中 setImmediate 比 setTimeout 先执行?
A I/O 回调在 poll 阶段执行,结束后直接进入 check 阶段(setImmediate),然后才到下一轮 timers 阶段(setTimeout),所以在 I/O 上下文中 setImmediate 必定先于 setTimeout。
Q 事件循环是单线程还是多线程?
A JavaScript 执行是单线程的,但底层 libuv 有线程池(默认 4 线程)处理 DNS 查询、文件 I/O 等操作,网络 I/O 由操作系统内核完成,完成后通知事件循环。
Q 如何避免阻塞事件循环?
A 将长计算拆分成小片用 setImmediate 分步执行;使用 Worker Threads 将 CPU 密集任务移到子线程;用异步 I/O 代替同步 I/O;用流(Stream)分块处理大数据。
Q process.nextTick 可以无限递归吗?
A 不建议。Node.js 有 process.maxTickDepth 默认值限制,递归 nextTick 会阻止事件循环进入下一阶段,导致 I/O 饥饿。应该用 setImmediate 代替递归 nextTick。
📖 小节
- 故事:被 200ms 拦住的服务的核心概念与使用方法
- 事件循环的 6 个阶段的核心概念与使用方法
- 微任务与宏任务的核心概念与使用方法
- setTimeout vs setImmediate vs process.nextTick的核心概念与使用方法
- 微任务 vs 宏任务对比的核心概念与使用方法
- 阻塞事件循环的危害与避免的核心概念与使用方法
- 综合示例:事件循环执行顺序验证脚本的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目