人人都会AI编程

宏任务与微任务的执行顺序与优先级

更新时间:2026-07-11

在 3.3.1 小节中我们已经梳理了事件循环的六大阶段,但仅仅了解阶段划分还不够。每一个阶段内部以及阶段之间,代码执行的先后顺序还受到宏任务微任务分类的严格控制。这也是 Node.js 中异步回调顺序最容易让人困惑的地方——表面上同样都是异步操作,为什么 Promise 回调有时会比 setTimeout 更早执行?为什么 process.nextTick 优先级似乎“无上限”?本节将把这些执行规则彻底理清。

宏任务(MacroTask)与微任务(MicroTask)的定义

这两个概念并非 Node.js 独创,浏览器中也存在相同的区分。简单来说:

  • 宏任务:由宿主环境(浏览器或 Node.js)发起的较大粒度的异步任务。在 Node.js 事件循环中,每一个阶段的回调执行都可以看作宏任务。例如 setTimeoutsetIntervalsetImmediateI/O 回调、新连接回调等,都属于宏任务。
  • 微任务:由 JavaScript 自身发起的更细粒度、更紧急的异步任务。主要包括 Promise.then/catch/finallyasync/await(本质是 Promise)、以及 Node.js 专属的 process.nextTick。微任务不会在事件循环的某个单独阶段处理,而是在每一个宏任务执行结束后,下一个宏任务开始前,一次性清空所有已注册的微任务队列

这也意味着,微任务队列的清理是“插队”进行的,它不遵守事件循环的六个阶段轮转,而是在两个宏任务之间的间隙立刻执行。

Node.js 中的微任务细分:nextTick 队列与 Promise 队列

Node.js 将微任务又细分为两个队列,并且给定了内部优先级:

  1. nextTick 队列:由 process.nextTick() 添加的回调。
  2. Promise 队列:由 Promise.then() 等添加的回调(通常也称为 MicroTask 队列,但在 Node.js 源码实现中两者分开)。

关键规则是:nextTick 队列的优先级高于 Promise 队列。 也就是说,每当一个宏任务执行完毕后,事件循环会先清空 nextTick 队列中的所有回调,然后清空 Promise 队列中的所有回调,之后再进入下一个宏任务。这一规则在 Node.js 文档中明确记载,也是导致 nextTick “优先级极高”的根源。

执行顺序实例拆解

下面的例子可以直观地展示执行顺序:

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

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

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

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

console.log('sync');

在 Node.js 中运行这段代码,输出为:

sync
nextTick
promise
timeout
immediate

(注:timeoutimmediate 的顺序可能会因为事件循环启动时机略有波动,但微任务 nextTickpromise 一定在它们之前)

我们逐步分析:

  1. 脚本作为宏任务整体执行,逐行同步代码 console.log('sync') 先输出。
  2. 执行过程中分别注册了 setTimeoutsetImmediatePromise.thennextTick,异步回调都被挂起。
  3. 脚本宏任务执行结束。此时 nextTick 队列中有一个回调,Promise 队列中也有一个回调。
  4. 事件循环清空微任务:先输出 nextTick,然后输出 promise
  5. 微任务清空后,事件循环继续,检查定时器阶段。setTimeout(fn, 0) 的定时器可能已到期,输出 timeout(如果事件循环启动较快,可能未到期而先进入 Poll 阶段,但本例输出 timeoutimmediate)。
  6. 继续循环,最终在处理 Check 阶段时输出 immediate

如果是这样:

setTimeout(() => {
  console.log('timeout');
  process.nextTick(() => console.log('timeout.nextTick'));
  Promise.resolve().then(() => console.log('timeout.promise'));
}, 0);

timeout 宏任务执行时,它内部又注册了微任务。由于微任务必须在当前宏任务结束之后立即清空,因此输出顺序是:

timeout
timeout.nextTick
timeout.promise

每个宏任务执行完毕都会触发微任务检查,而不是等到整个事件循环轮询一圈。这一点在复杂逻辑中至关重要。

process.nextTick 的“饥饿”风险

由于 nextTick 队列拥有最高优先级,并且会在每次清空时一次性全部执行,在 nextTick 中递归调用 process.nextTick 会导致事件循环永远无法进入下一个阶段,从而造成 I/O 和定时器回调无法执行,这种现象称为“饿死事件循环”

// 危险示例:永远阻塞事件循环
function recurse() {
  process.nextTick(recurse);
}
recurse();
// 后续任何 setTimeout、I/O 都不会被执行

setImmediate 则不会导致此问题,因为它在 Check 阶段执行,只会在下一个循环轮次中触发。因此除非明确需要最高优先级的微任务插入,推荐优先使用 setImmediatePromise,避免滥用 nextTick

与浏览器事件循环的核心差异

浏览器的事件循环中,每个 <script> 标签执行、setTimeout、UI 渲染等为宏任务,Promise、MutationObserver 为微任务。但浏览器有额外的渲染步骤,微任务通常在执行后紧跟着渲染机会。而 Node.js 没有 UI 渲染,因此微任务纯粹为了协调异步代码优先级。

最显著的区别在于 setImmediate 是 Node.js 独有,它被定义为在当前事件循环的 Check 阶段执行,与 setTimeout(fn,0) 的调用时机非常接近,但执行阶段不同。另一个差异是 process.nextTick 的存在,让 Node.js 的微任务调度比浏览器更灵活,也更易踩坑。

常见面试题的逻辑拆解

经典问题:

async function foo() {
  console.log('1');
  await bar();
  console.log('2');
}
async function bar() {
  console.log('3');
}
foo();
console.log('4');

输出为 1 3 4 2。因为 await bar() 相当于 Promise.resolve(bar()).then(() => console.log('2')),所以 console.log('2') 作为微任务,在当前宏任务(脚本)结束后清空微任务时执行,先于后续宏任务但晚于同步的 console.log('4')

另一个经典:

setTimeout(() => console.log(1), 0);
setImmediate(() => console.log(2));

由于 setTimeout(fn,0)setImmediate 的执行顺序在不同 Node.js 版本和运行环境下可能不稳定。当主模块直接运行时,事件循环在定时器阶段可能还未到 1ms 的阈值,导致 setTimeout 回调被跳过而进入 Poll 阶段再执行 setImmediate;但如果在 I/O 回调中调用两者,则 setImmediate 总是在 setTimeout(fn,0) 之前执行(因为 Check 阶段紧跟在 Poll 之后),这属于事件循环内部顺序的细节。

实际开发中的应用策略

  • 善用微任务完成“异步收尾”:在某个异步操作后需要更新状态或触发后续逻辑时,使用 Promise.resolve().then() 可以将处理延迟到当前宏任务结束后、下一个宏任务之前,避免阻塞或时序错乱。
  • setImmediate 拆分 CPU 密集循环:当需要在循环中释放事件循环时,在每次迭代中通过 setImmediate 推迟下一次循环,可以保持对其他请求的响应能力。
  • 避免在 nextTick 中执行重逻辑:因为 nextTick 的高优先级和执行时机,回调中的代码会延迟所有 I/O 和定时器。除非确切知道其必要性,否则用 setImmediate 更安全。
  • 理解 Promise 的微任务特性来优化性能:例如批量状态更新可以借助 Promise 微任务将多次更新合并到一次事件循环尾端,减少不必要的中间状态渲染(在 Node.js 服务端中减少不必要的响应写操作)。

总结一张图

宏任务执行
      ↓
  当前宏任务结束
      ↓
清空 nextTick 队列(直到队列空)
      ↓
清空 Promise 微任务队列(直到队列空)
      ↓
是否还有微任务? ——> 是 ——> 继续清空
      ↓ 否
进入下一个宏任务(事件循环下一阶段或同阶段的下一回调)

这一套执行模型是 Node.js 异步编程的心智模型核心。掌握了微任务与宏任务的执行顺序和优先级,就拥有了预测任意异步代码执行时序的能力,也是编写高性能、无阻塞 Node.js 应用的前提之一。