~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月4日

我不知道的 V8(14)— 微任务:执行时机、优先级与底层实现

"Promise 的 then 回调比 setTimeout 先执行"——大多数 JavaScript 开发者知道这个结论,但很少有人能说清楚为什么。这不是约定俗成的规则,而是 V8 内部有具体的执行机制在支撑:MicrotaskQueue 数据结构 + PerformMicr…

“Promise 的 then 回调比 setTimeout 先执行”——大多数 JavaScript 开发者知道这个结论,但很少有人能说清楚为什么。这不是约定俗成的规则,而是 V8 内部有具体的执行机制在支撑:MicrotaskQueue 数据结构 + PerformMicrotaskCheckpoint 触发点。理解这套机制,才能解释微任务的所有行为,包括一些反直觉的边界情况。

一、微任务包括哪些

V8 中的微任务(Microtasks)包含三类:

  • Promise 回调Promise.then()Promise.catch()Promise.finally()
  • queueMicrotask():直接向微任务队列添加任务的 API
  • MutationObserver 回调:DOM 变化后的通知回调

这三类任务有一个共同点:它们的执行时机是”当前宏任务结束后,立即执行,优先于下一个宏任务”。

二、为什么微任务优先于宏任务

先看经典的执行顺序例子:

setTimeout(() => console.log('Macro'), 0);
Promise.resolve().then(() => console.log('Micro 1'));
Promise.resolve().then(() => console.log('Micro 2'));
console.log('Sync');

// 输出顺序:
// Sync       ← 同步代码最先
// Micro 1    ← 微任务,宏任务前执行
// Micro 2    ← 微任务,在同一轮清空
// Macro      ← 宏任务,所有微任务之后

这个顺序背后是 V8 的事件循环规则:每个宏任务执行完毕后,在进入下一个宏任务之前,清空整个微任务队列

微任务的优先级不是”比宏任务快一点”,而是”本轮宏任务结束前必须全部完成”。

三、V8 的底层实现:MicrotaskQueue

微任务队列在 V8 的 C++ 层是一个叫 MicrotaskQueue 的结构,存储在 V8 的 Isolate(运行时实例)中。

创建过程:

Promise.resolve().then(() => console.log('Micro'));

调用 then 时,V8 创建一个 PromiseReaction 对象,通过 EnqueueMicrotask 函数将其推入 MicrotaskQueuequeueMicrotask 是对这个接口的直接暴露:

queueMicrotask(() => console.log('Direct micro'));
// 等价于内部的 EnqueueMicrotask 调用

清空过程:

V8 的核心函数是 PerformMicrotaskCheckpoint。它在以下时机被调用:

  • 当前宏任务(或脚本)执行完毕,调用栈清空时
  • 显式调用 queueMicrotask 后的检查点
  • 某些 Promise 操作后

PerformMicrotaskCheckpoint 的逻辑:检查 MicrotaskQueue 是否为空,不为空则从头部依次取出并执行,直到队列清空。

关键细节:执行微任务时产生的新微任务,也会在本轮清空

Promise.resolve()
  .then(() => {
    console.log('Micro 1');
    // 在微任务里再加一个微任务
    return Promise.resolve('Micro 2');
  })
  .then((v) => console.log(v));

// 输出:
// Micro 1
// Micro 2   ← 在同一轮微任务清空中执行,不会推到下个宏任务

四、微任务过多时的陷阱

“微任务在宏任务后全部清空”意味着一个风险:如果微任务不断产生新的微任务,事件循环会被卡住,下一个宏任务永远无法开始。

setTimeout(() => {
  console.log('这行永远不会执行');
}, 0);

// 无限递归的微任务
function infiniteMicrotask() {
  queueMicrotask(infiniteMicrotask);
}
infiniteMicrotask(); // 页面卡死

更常见的场景是微任务数量过多导致延迟:

// Bad Case:在一个宏任务中产生大量微任务
setTimeout(() => {
  for (let i = 0; i < 10000; i++) {
    Promise.resolve().then(() => processItem(i));
  }
}, 0);
// 下一个宏任务(如 setTimeout、UI 渲染)要等这 10000 个微任务全部执行完

// Better:分批推入宏任务队列
function processBatch(start, end) {
  for (let i = start; i < end; i++) {
    processItem(i);
  }
  if (end < 10000) {
    setTimeout(() => processBatch(end, Math.min(end + 100, 10000)), 0);
  }
}
processBatch(0, 100);

五、queueMicrotask 的实际用途

queueMicrotask 是 ES2020 引入的标准 API,直接向微任务队列添加任务,不需要通过 Promise 包装。

适合替代 setTimeout(fn, 0) 的场景:

// 旧写法:用 setTimeout 推迟到"下一帧"
setTimeout(() => updateUI(), 0);

// 问题:setTimeout 是宏任务,会等到下一个事件循环才执行
// 如果当前宏任务后还有其他宏任务(如 setInterval),UI 更新会被推得更远

// 新写法:queueMicrotask 在当前宏任务后立即执行
queueMicrotask(() => updateUI());
// 无论有多少宏任务在排队,这次 UI 更新都会在当前宏任务结束后立即发生

批量异步操作的微任务优化:

// 场景:多次 DOM 操作后,一次性做检查
let pendingUpdate = false;

function scheduleUpdate() {
  if (!pendingUpdate) {
    pendingUpdate = true;
    queueMicrotask(() => {
      // 统一在微任务里处理所有 DOM 变化后的检查
      performUpdate();
      pendingUpdate = false;
    });
  }
}

// 即使连续调用多次,实际的 performUpdate 只会执行一次
scheduleUpdate();
scheduleUpdate();
scheduleUpdate();

这个模式在 Vue、React 的调度器里随处可见——把多次状态更新”合批”到一次微任务中处理,避免重复渲染。

表单验证的即时响应:

// 动态添加输入框后,立即验证表单状态
document.body.appendChild(inputElement);
queueMicrotask(() => {
  // 在 DOM 添加完成后、渲染前执行验证
  validateForm();
});

六、微任务 vs 宏任务 vs 同步代码:选哪个

场景推荐方式原因
必须立即执行同步代码无延迟
当前宏任务结束后立即执行queueMicrotask / Promise.then在渲染前完成,响应最快
延迟到下一轮事件循环setTimeout(fn, 0)给渲染和其他宏任务腾出空间
固定间隔重复执行setInterval / requestAnimationFrame周期性任务
DOM 变化后响应MutationObserver批量变化,微任务级别的即时性

七、总结

微任务的核心机制:调用 thenqueueMicrotask 时将回调放入 MicrotaskQueue,当前宏任务(或脚本)执行完毕后,V8 调用 PerformMicrotaskCheckpoint 清空整个队列

这套机制的设计目标:让”需要紧跟当前操作的回调”(如 Promise 结果处理、DOM 更新通知)不用等待下一个宏任务轮次,在当前轮次内就完成。

queueMicrotask 的直接暴露让开发者可以不依赖 Promise,直接向微任务队列添加任务——在实现框架级别的调度器、批量更新、延迟验证等场景时,这是比 setTimeout 更精确的工具。


本系列其他文章:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;