Node.js 事件循环深度解析:从原理到实战,彻底搞懂异步机制

By | 2026年7月20日

一、为什么你需要理解事件循环?

在 Node.js 中,事件循环是异步编程的核心。如果你曾为 setTimeoutPromise 的执行顺序感到困惑,或者遇到过 process.nextTick 导致的性能问题,那么这篇文章就是为你准备的。我们将从事件循环的六个阶段入手,配合代码实验,彻底搞懂异步任务的调度机制。

二、事件循环的六个阶段

Node.js 的事件循环由 libuv 库实现,主要包含以下阶段(按顺序执行):

  1. timers:执行 setTimeoutsetInterval 的回调。
  2. pending callbacks:执行延迟到下一轮的 I/O 回调。
  3. idle, prepare:内部使用。
  4. poll:获取新的 I/O 事件,执行 I/O 回调。
  5. check:执行 setImmediate 的回调。
  6. close callbacks:执行关闭事件的回调(如 socket.on('close'))。

每个阶段都有一个先进先出的回调队列。当队列为空或达到最大限制时,事件循环会进入下一个阶段。

三、微任务与宏任务

在 Node.js 中,任务分为两类:

  • 宏任务(MacroTask):由事件循环的各个阶段处理,如 setTimeoutsetIntervalsetImmediate、I/O 回调。
  • 微任务(MicroTask):在每个宏任务完成后立即执行,包括 process.nextTickPromise.then/catch/finally

注意process.nextTick 的优先级高于 Promise 微任务,它是“微任务中的微任务”。

四、实战:代码实验

实验 1:基本执行顺序


console.log('1: start');

setTimeout(() => {
    console.log('2: setTimeout');
}, 0);

setImmediate(() => {
    console.log('3: setImmediate');
});

process.nextTick(() => {
    console.log('4: nextTick');
});

Promise.resolve().then(() => {
    console.log('5: Promise');
});

console.log('6: end');

输出结果


1: start
6: end
4: nextTick
5: Promise
2: setTimeout
3: setImmediate

解释

  • 同步代码先执行,打印 1 和 6。
  • 微任务在宏任务之前执行:process.nextTick 优先于 Promise。
  • 宏任务中,setTimeout 在 timers 阶段执行,setImmediate 在 check 阶段执行。由于 timers 阶段先于 check,所以 2 先于 3。

实验 2:setTimeout vs setImmediate 的微妙差异


const fs = require('fs');

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('1: setTimeout');
    }, 0);

    setImmediate(() => {
        console.log('2: setImmediate');
    });
});

输出结果(几乎总是)


2: setImmediate
1: setTimeout

解释:当在 I/O 回调中(poll 阶段)注册 setTimeoutsetImmediate 时,poll 阶段之后直接进入 check 阶段,因此 setImmediate 先执行。而 setTimeout 需要等到下一轮 timers 阶段。

💡 踩坑提示:如果不在 I/O 回调中,setTimeoutsetImmediate 的执行顺序可能不确定,因为主模块的启动时间可能影响 timers 的触发。

实验 3:process.nextTick 的递归陷阱


function recursiveNextTick() {
    process.nextTick(() => {
        console.log('nextTick');
        recursiveNextTick(); // 递归调用
    });
}

recursiveNextTick();

setTimeout(() => {
    console.log('setTimeout');
}, 100);

输出


nextTick
nextTick
nextTick
...(无限循环,永远不会执行 setTimeout)

解释process.nextTick 会在当前阶段结束前反复执行,导致事件循环永远无法进入下一阶段。因此,避免在 process.nextTick 中递归或使用长循环

五、最佳实践

  1. 优先使用 setImmediate 而非 setTimeout(fn, 0)setImmediate 在 check 阶段执行,性能更优,且不受定时器精度影响。
  2. 避免滥用 process.nextTick:它可能阻塞事件循环。建议使用 setImmediatePromise 来推迟任务。
  3. 理解微任务的执行时机:微任务会在每个宏任务后立即执行,包括 process.nextTick 和 Promise。因此,可以在微任务中处理高优先级的异步操作。
  4. 使用 async/await 时注意await 之后的代码相当于 Promise.then,属于微任务。

六、总结

Node.js 事件循环是异步编程的基石。通过理解六个阶段和微任务/宏任务的执行顺序,你可以编写出更高效、更可预测的代码。记住:

  • 同步代码 > 微任务(nextTick > Promise) > 宏任务(timers > I/O > check > close)
  • 在 I/O 回调中,setImmediate 优先于 setTimeout
  • 谨慎使用 process.nextTick,避免递归

下一步,你可以阅读 Node.js 官方文档关于 Event Loop 的详细说明,或者深入 libuv 源码。