一、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 微任务队列 | 宿主宏任务队列 | 输出 |
|---|---|---|---|---|
| 1 | log('1') | - | - | 1 |
| 2 | setTimeout | - | cb2 | - |
| 3 | Promise.then | [cb3] | cb2 | - |
| 4 | log('5') | [cb3] | cb2 | 5 |
| 5 | 栈空 → RunMicrotasks → cb3 执行 | → cb4 入队 | cb2 | 3 |
| 6 | RunMicrotasks 继续 → cb4 执行 | [] | cb2 | 4 |
| 7 | size_=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 关联 | 无 | FulfillPromise → EnqueueMicrotask |
| 插队行为 | 不能 | 新增微任务在本轮立即执行 |
核心口诀:同步代码 → V8 清空微任务(
while size_>0)→ 宿主取下一个宏任务 → V8 清空微任务 → ...
参考源码:microtask-queue.cc · promise-then.tq · Node.js task_queues.js