从 V8 引擎视角理解微任务与宏任务

13 阅读4分钟

一、V8 引擎的基本架构

V8 是 Google 开发的开源 JavaScript 引擎,用于 Chrome 和 Node.js 中。

┌─────────────────────────────────────────────────────┐
│                    V8 引擎                           │
│  ┌──────────────┐     ┌─────────────────────────┐  │
│  │  调用栈        │     │  微任务队列(V8 原生维护)  │  │
│  │  Call Stack  │     │    MicrotaskQueue       │  │
│  └──────────────┘     └─────────────────────────┘  │
└─────────────────────────────────────────────────────┘
         │ 协作
         ▼
┌─────────────────────────────────────────────────────┐
│              宿主环境(浏览器 / Node.js)               │
│  ┌──────────────────────────────────────────────┐   │
│  │       宏任务队列 + 事件循环 Event Loop          │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

关键点:微任务队列由 V8 原生维护,宏任务队列和事件循环由宿主环境实现(浏览器的 Chromium / Node.js 的 libuv)。


二、微任务队列:V8 源码视角

2.1 MicrotaskQueue 的数据结构

V8 源码 src/execution/microtask-queue.h 中定义了微任务队列:

// src/execution/microtask-queue.h(精简)
class MicrotaskQueue {
 public:
  void EnqueueMicrotask(Microtask microtask);  // 入队
  int  RunMicrotasks(Isolate* isolate);        // 清空执行

 private:
  intptr_t* ring_buffer_;  // 底层:环形缓冲区(入队/出队 O(1))
  intptr_t  capacity_;     // 容量
  intptr_t  size_;         // 当前任务数
  intptr_t  start_;        // 队头指针
};

关键设计:底层用环形缓冲区而非普通数组,入队/出队都是 O(1),避免频繁内存分配。


2.2 RunMicrotasks:微任务是怎么执行的

src/execution/microtask-queue.cc 中的核心执行逻辑:

// src/execution/microtask-queue.cc(精简关键逻辑)
int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {
  while (size_ > 0) {           // ← 循环直到队列彻底为空
    Microtask task = GetMicrotask(isolate, start_);
    start_++;
    size_--;
    MicrotaskV8Task(isolate, task).Call(isolate);
    // 执行过程中新入队的微任务会使 size_ 变大,循环继续
  }
  return 0;
}

这段代码直接解释了一个重要行为:微任务执行期间产生的新微任务,会在本轮一并清空——因为 while (size_ > 0) 会持续检测队列是否为空。


2.3 微任务的触发时机:Checkpoint

V8 通过 MicrotasksPolicy 枚举控制微任务何时被触发:

// src/execution/isolate.h(精简)
enum class MicrotasksPolicy {
  kExplicit,  // 宿主显式调用(Node.js 早期方式)
  kScoped,    // 作用域结束时执行
  kAuto       // 默认:调用栈清空时自动执行 ← 浏览器和现代 Node.js
};

V8 暴露 PerformMicrotaskCheckpoint() 接口给宿主环境:宿主每执行完一个宏任务,就调用它通知 V8 去清空微任务队列。这是宏任务和微任务协作的关键接口


三、Promise 与微任务的关联

Promise 的 .then() 为什么是微任务?答案在 V8 的 Promise 实现里。

// src/builtins/promise-then.tq(Torque,V8 内置函数描述语言,精简)
// Promise resolve 时调用此函数
transitioning builtin FulfillPromise(promise, value) {
  const reactions = promise.reactions_or_result;

  // ★ 将 .then() 的回调包装成 PromiseReactionTask,入队微任务
  MicrotaskQueueEnqueueMicrotask(
    context,
    PromiseReactionTask { reaction: reactions, argument: value }
  );
}

白话解释Promise.resolve().then(fn) 并不立刻执行 fn,而是将 fn 封装成 PromiseReactionTask,调用 EnqueueMicrotask 放入 V8 的微任务队列,等调用栈清空后再由 RunMicrotasks 执行。


四、宏任务:宿主环境的调度

宏任务不在 V8 内部,由宿主环境维护。以 Node.js 为例,libuv 驱动宏任务,执行完毕后通知 V8:

// Node.js 核心调度(大幅精简示意)
do {
  uv_run(event_loop, UV_RUN_DEFAULT);     // 1. 执行一个宏任务(libuv)
  isolate->PerformMicrotaskCheckpoint();  // 2. 通知 V8 清空微任务 ← 关键接口
} while (more_tasks);

两套队列分属不同系统,由 PerformMicrotaskCheckpoint() 这一接口联结,完成协作。


五、完整事件循环流程

[宿主环境 libuv/Chromium]              [V8 引擎]
         │                                 │
         │  取出一个宏任务 → 交给 V8        │
         │ ──────────────────────────────► │ 执行同步代码,调用栈清空
         │                                 │
         │  PerformMicrotaskCheckpoint()   │
         │ ◄────────────────────────────── │ RunMicrotasks() while(size_>0)
         │                                 │ 新产生的微任务也在此轮清空
         │                                 │
         │  (可选) UI 渲染                  │
         │  取下一个宏任务...               │

六、async/await 在 V8 中的实现

async/await 是语法糖,V8 编译时将其转换为 Promise 状态机。

// 你写的代码
async function foo() {
  console.log('A');
  await bar();
  console.log('C');  // await 后的代码
}

V8 内部概念等价:

function foo() {
  console.log('A');
  return bar().then(() => {
    console.log('C');  // 被包装为 PromiseReactionTask → 微任务
  });
}

V8 用 ResumeGenerator 内置函数处理 await 的恢复:await 暂停时将后续逻辑注册为 Promise 回调(EnqueueMicrotask),恢复时从微任务队列取出执行。

结论await 的暂停和恢复,本质是两次微任务队列的入队出队


七、经典代码示例解析

console.log('1');

setTimeout(() => console.log('2'), 0);   // 宿主宏任务队列

Promise.resolve()
  .then(() => console.log('3'))          // V8 EnqueueMicrotask → cb3
  .then(() => console.log('4'));         // cb3 完成后 EnqueueMicrotask → cb4

console.log('5');

// 输出:1 → 5 → 3 → 4 → 2
步骤调用栈V8 微任务队列宿主宏任务队列输出
1log('1')--1
2setTimeout-cb2-
3Promise.then[cb3]cb2-
4log('5')[cb3]cb25
5栈空 → RunMicrotasks → cb3 执行cb4 入队cb23
6RunMicrotasks 继续 → cb4 执行[]cb24
7size_=0 退出 → 宿主取 cb2--2

八、Node.js 特殊性:process.nextTick

process.nextTick 不走 V8 的微任务队列,而是 Node.js 在调用 PerformMicrotaskCheckpoint 之前,先清空自己维护的 nextTick 队列:

// Node.js 内部调度顺序(lib/internal/process/task_queues.js,精简)
function processTicksAndRejections() {
  drainNextTicks();  // 1. 先清空 nextTick 队列(Node.js 自己维护)
  runMicrotasks();   // 2. 再触发 V8 清空 Promise 微任务
}
process.nextTick(() => console.log('nextTick'));      // Node.js 独立队列
Promise.resolve().then(() => console.log('Promise')); // V8 微任务队列
// 输出:nextTick → Promise

九、总结

维度宏任务微任务
维护者宿主环境(libuv / Chromium)V8 引擎(MicrotaskQueue
每轮执行数量取出一个全部清空while size_ > 0
触发方式宿主事件循环调度PerformMicrotaskCheckpoint()
Promise 关联FulfillPromiseEnqueueMicrotask
插队行为不能新增微任务在本轮立即执行

核心口诀:同步代码 → V8 清空微任务(while size_>0)→ 宿主取下一个宏任务 → V8 清空微任务 → ...


参考源码:microtask-queue.cc · promise-then.tq · Node.js task_queues.js