事件循环总览
事件循环是一个无限循环,每一轮(tick)按固定顺序经过若干阶段,每个阶段有一个 FIFO 回调队列。某个阶段队列清空后进入下一阶段,所有阶段走完再回到开头。
| 阶段 | 处理的回调 |
|---|---|
| timers | setTimeout / setInterval 到期的回调 |
| pending | 上一轮延迟的 I/O 回调(如 TCP 错误) |
| poll | 取回新的 I/O 事件、执行 I/O 回调 |
| check | setImmediate 回调 |
| close | close 事件(如 socket.destroy) |
宏任务与微任务
微任务(Promise.then/catch/finally、queueMicrotask)不属于上述任何阶段,而是在每个阶段之间、阶段队列清空后立即清空。所以微任务总优先于下一个宏任务。
console.log('1 同步');
setTimeout(() => console.log('4 timers 宏任务'), 0);
Promise.resolve().then(() => console.log('3 微任务'));
console.log('2 同步');
// 输出顺序:1 同步 → 2 同步 → 3 微任务 → 4 timers 宏任务一旦进入微任务队列,会一直清空到空才继续下一阶段。微任务里再产生微任务会持续执行,可能饿死 I/O,需谨慎。
timers 阶段
setTimeout(fn, 0) 不保证 0 毫秒后执行,而是"至少 delay 毫秒后、在 timers 阶段"执行。受事件循环繁忙程度影响。
const start = Date.now();
setTimeout(() => {
console.log('过了', Date.now() - start, 'ms');
}, 0);
// 若主线程被长循环占用,实际延时会远大于 0poll 与 check 阶段
poll 阶段取回新的 I/O 回调并执行;若队列空且存在 setImmediate,则进入 check 阶段。poll 会计算"应阻塞多久"以尽快触发最近的定时器。
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 在 I/O 回调中:setImmediate 先执行(check 阶段就在 poll 之后)setTimeout vs setImmediate
两者顺序在不同场景下不同:在 I/O 回调内 setImmediate 先跑;在模块顶层则 setTimeout 通常先跑(因为 timers 阶段早于 check)。
需要"在当前操作之后、I/O 之前"执行,用 setImmediate;需要"延迟至少 N 毫秒"用 setTimeout。顺序不确定时不要用两者互相依赖。
process.nextTick
process.nextTick 不属于事件循环任何阶段,而是在当前操作完成后、事件循环继续前立即执行。比微任务还早。
Promise.resolve().then(() => console.log('微任务'));
process.nextTick(() => console.log('nextTick'));
// 输出:nextTick → 微任务递归调用 nextTick 会永远不让事件循环前进,导致 I/O 饿死。Node 有默认的 maxWarnings 警告但仍会阻塞。
阻塞主线程的危害
任何同步耗时操作(大循环、JSON.parse 大对象、同步文件读、crypto 计算)都会阻塞整个事件循环,使所有请求卡住。
// 反例:同步压缩大文件会阻塞所有请求
const zlib = require('node:zlib');
const out = zlib.gzipSync(hugeBuffer); // 阻塞!
// 正解:用异步 + 流
const out = hugeStream.pipe(createGzip());重计算请用 worker_threads(见性能章),把工作移出主线程,保持事件循环可响应。
常见陷阱与 FAQ
在 I/O 回调里,先经历 poll 阶段(执行 I/O 回调),紧接着就是 check 阶段(setImmediate),而 timers 要等下一轮才到,所以 setImmediate 先执行。
微任务。它在阶段之间清空,优先于任何宏任务(包括 setTimeout)。
是的。await 之后的代码等价于 Promise.then 的微任务,因此在当前阶段结束后、下一宏任务前执行。