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

一、V8 引擎的基本架构

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

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

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


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

2.1 MicrotaskQueue 的数据结构

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

cpp 复制代码
// 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 中的核心执行逻辑:

cpp 复制代码
// 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 枚举控制微任务何时被触发:

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

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


三、Promise 与微任务的关联

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

cpp 复制代码
// 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:

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

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


五、完整事件循环流程

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

六、async/await 在 V8 中的实现

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

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

V8 内部概念等价:

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

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

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


七、经典代码示例解析

javascript 复制代码
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 队列:

js 复制代码
// Node.js 内部调度顺序(lib/internal/process/task_queues.js,精简)
function processTicksAndRejections() {
  drainNextTicks();  // 1. 先清空 nextTick 队列(Node.js 自己维护)
  runMicrotasks();   // 2. 再触发 V8 清空 Promise 微任务
}
javascript 复制代码
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

相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马6 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端