心智模型
Node 是单线程的——同一时刻只跑一段 JS。异步靠事件循环调度。
┌─────────────────────────────────┐
│ Call Stack │ ← 现在跑的代码
└─────────────────────────────────┘
↑
│ 取下一个任务
┌───────────┴─────────────────────┐
│ Event Loop │ ← 调度器
└─────────────────────────────────┘
↑ ↑
│ │
┌─────┴───┐ ┌────┴──────┐
│ Tasks │ │ Micro │ ← 两种队列
│ Queue │ │ tasks │
└─────────┘ └───────────┘
事件循环的 6 个阶段
┌─────────────────────────────┐
│ timers │ setTimeout / setInterval 到期
├─────────────────────────────┤
│ pending callbacks │ 系统操作(如 TCP 错误)
├─────────────────────────────┤
│ idle, prepare │ 内部
├─────────────────────────────┤
│ poll │ 拉新 I/O 事件,执行回调
├─────────────────────────────┤
│ check │ setImmediate 回调
├─────────────────────────────┤
│ close callbacks │ socket.on('close') 等
└─────────────────────────────┘
每个阶段之间清空 microtask 队列
每轮循环按顺序经过这 6 个阶段——叫一个 "tick"。
任务 vs 微任务
Microtask:
Promise.then / catch / finallyqueueMicrotask(fn)process.nextTick(fn)← Node 独有,比微任务还优先
Macrotask / 任务:
setTimeout/setIntervalsetImmediate- I/O 回调(fs / net 等)
执行顺序
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
queueMicrotask(() => console.log('3')); // 微任务
Promise.resolve().then(() => console.log('4')); // 微任务
process.nextTick(() => console.log('5')); // nextTick(Node 优先)
console.log('6'); // 同步
输出:
1
6
5 ← nextTick 最先(在所有微任务前)
3 ← microtask
4 ← microtask
2 ← 等当前同步代码 + 微任务全跑完,才轮到 setTimeout
记忆:
同步代码 → process.nextTick → microtask(Promise / queueMicrotask) → 进入循环阶段
setImmediate vs setTimeout(fn, 0)
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
输出顺序不确定(看启动时机)。
但在 I/O 回调里:
fs.readFile('a.txt', () => {
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
});
immediate 永远先——因为 I/O 阶段后紧接 check 阶段。
process.nextTick:双刃剑
process.nextTick(() => {
// 在当前阶段结束、microtask 之前执行
});
特点:
- 比 microtask 还早
- 用于"在当前操作完成后立即跑"
⚠ 递归调用会饿死事件循环:
function bad() {
process.nextTick(bad); // I/O 永远轮不到
}
bad(); // 系统假死
实战很少手动 nextTick——库内部用得多。
queueMicrotask vs Promise.then
queueMicrotask(() => console.log('a'));
Promise.resolve().then(() => console.log('b'));
两者都是微任务,顺序是注册顺序。queueMicrotask 更轻量——不创建 Promise 对象。
阻塞事件循环 = 性能杀手
// ❌ CPU 密集同步代码 → 阻塞循环
function fibonacci(n) {
if (n < 2) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
server.on('request', () => {
fibonacci(40); // 阻塞 1+ 秒,期间所有请求挂起
});
解决:
- 把 CPU 密集任务拆成小块(每几 ms 让出循环
setImmediate) - 用 worker_threads 真多线程(见 24-worker-threads)
- 用 child_process 起子进程
看事件循环延迟
import { monitorEventLoopDelay } from 'perf_hooks';
const h = monitorEventLoopDelay();
h.enable();
setInterval(() => {
console.log('Mean delay ns:', h.mean); // 越高越糟
h.reset();
}, 1000);
平均延迟 > 几十毫秒 → 事件循环卡了。
坑
- 阻塞循环 = 全站慢:任何长同步代码都会拖累所有连接
process.nextTick递归 / 大量调用饿死循环- 现代代码不需要手动 setImmediate / nextTick——理解概念知道顺序就行
setTimeout(fn, 0)最小延迟实际是 4ms(在某些情况下)
至此完成异步基础 4 篇。下一篇起进入核心 API:fs / path / streams / events / http 等。