LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

搞懂 JS 异步的底层真相:从 V8 源码看微任务与宏任务

freeflydom
2026年3月9日 15:28 本文热度 51

任务队列不是一个,执行顺序不是你以为的那样。本文结合 V8、Chromium、Node.js 源码,彻底讲清楚异步任务的调度本质。所有代码均经过源码核查,每处均附对应链接。


一、全局视角:谁在管理任务?

┌──────────────────────────────────────────────────────────────────┐
│                          V8 引擎                                  │
│  ┌─────────────────┐        ┌──────────────────────────────┐    │
│  │   调用栈          │        │  微任务队列 MicrotaskQueue    │    │
│  │   Call Stack    │        │  (环形缓冲区,V8 原生维护)     │    │
│  └─────────────────┘        └──────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────┘
                    │ PerformCheckpoint() / PerformMicrotaskCheckpoint()
                    ▼
┌──────────────────────────────────────────────────────────────────┐
│                       宿主环境                                    │
│  ┌───────────────────────┐    ┌───────────────────────────┐     │
│  │       浏览器            │    │        Node.js            │     │
│  │  Blink Scheduler      │    │    libuv 事件循环          │     │
│  │  - 多优先级任务队列     │    │  - timers                 │     │
│  │  - Render Pipeline    │    │  - pending/idle/prepare   │     │
│  │  - rAF 队列           │    │  - poll / check / close   │     │
│  └───────────────────────┘    │  - nextTick Queue(额外)  │     │
│                               └───────────────────────────┘     │
└──────────────────────────────────────────────────────────────────┘

核心分工:V8 维护调用栈 + 微任务队列;宿主环境维护宏任务队列 + 事件循环,两者通过 PerformCheckpoint 接口联结。


二、V8 内部:微任务队列的实现

数据结构:环形缓冲区

源码在 src/execution/microtask-queue.h

// src/execution/microtask-queue.h
// https://chromium.googlesource.com/v8/v8/+/lkgr/src/execution/microtask-queue.h
class MicrotaskQueue final : public v8::MicrotaskQueue {
 public:
  int RunMicrotasks(Isolate* isolate);           // 清空执行
  void EnqueueMicrotask(Tagged<Microtask> microtask); // 入队
  intptr_t capacity() const { return capacity_; }
  intptr_t size() const { return size_; }
  intptr_t start() const { return start_; }
 private:
  // 环形缓冲区注释原文:
  // ring_buffer_[(start_ + i) % capacity_] contains |i|th Microtask
  intptr_t size_ = 0;      // 当前任务数
  intptr_t capacity_ = 0;
  intptr_t start_ = 0;     // 队头指针
  Address* ring_buffer_ = nullptr;
};

RunMicrotasks:微任务的执行机制

现代 V8 的 RunMicrotasks 不是一个简单的 C++ while 循环,而是委托给 CSA(CodeStubAssembler)内置函数 RunMicrotasksDrainQueue 执行,这是一次性能优化——将 JS 与 C++ 之间的切换降到最少(约 60% 的性能提升):

// src/execution/microtask-queue.cc(精简)
// https://chromium.googlesource.com/v8/v8/+/lkgr/src/execution/microtask-queue.cc
int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {
  // 实际调用 Execution::RunMicrotasks()
  // 后者会进入 CSA 内置函数 RunMicrotasksDrainQueue
  // 循环逻辑在 CSA 中实现,直到 size_ 归零
  MaybeHandle<Object> maybe_result =
      Execution::RunMicrotasks(isolate, ...);
  // 执行终止时的清理
  if (maybe_result.is_null() && maybe_exception.is_null()) {
    size_ = 0; start_ = 0; capacity_ = 0;
    return -1;
  }
  return finished_microtask_count_;
}

连锁执行的本质:CSA 内置函数在处理每个微任务前都会检查 size_,执行过程中若新产生微任务(size_ 增大),会继续循环,直到队列彻底清空。

微任务触发时机:MicrotasksPolicy

// include/v8-microtask-queue.h
// https://chromium.googlesource.com/v8/v8/+/lkgr/include/v8-microtask-queue.h
enum class MicrotasksPolicy {
  kExplicit,  // 宿主显式调用
  kScoped,    // 作用域退出时
  kAuto       // 调用栈清空自动触发 ← 默认值,见 microtask-queue.h
              // microtasks_policy_ = v8::MicrotasksPolicy::kAuto
};

V8 暴露给宿主的触发入口是 MicrotaskQueue::PerformCheckpoint(v8::Isolate*),宿主每完成一个任务,就调用它触发微任务清空。


三、Promise 与微任务的关联

.then() 的回调为什么是微任务?真实的调用链:

Promise.resolve()
  → FulfillPromise()          ← 修改 Promise 状态
    → TriggerPromiseReactions()  ← 触发所有 .then 回调
      → EnqueueMicrotask()    ← ★ 真正入队微任务

入队发生在 promise-abstract-operations.tq

// src/builtins/promise-abstract-operations.tq
// https://github.com/v8/v8/blob/main/src/builtins/promise-abstract-operations.tq
// .then(fn) 的回调被包装成 PromiseReactionJobTask 入队
EnqueueMicrotask(handlerContext, promiseReactionJobTask);

关键认知.then(fn) 注册时,fn 只是挂在 Promise 对象上,不在任何队列里。只有 Promise 被 resolve 的那一刻,TriggerPromiseReactions 才将 fn 包装成 PromiseReactionJobTask 放入微任务队列。网络请求的回调为什么"等请求完成才入队",原因正在于此。


四、浏览器的事件循环

浏览器事件循环遵循 HTML Living Standard,由 Blink Scheduler 驱动。

浏览器的任务队列:多任务源

Blink 定义了 80+ 种任务类型(TaskType 枚举),每种任务源有独立的队列和优先级:

// third_party/blink/public/platform/task_type.h
// https://chromium.googlesource.com/chromium/src/third_party/+/master/blink/public/platform/task_type.h
enum class TaskType : unsigned char {
  kUserInteraction = 2,      // 用户交互(点击、键盘)← 高优先级
  kNetworking = 3,           // 网络响应(fetch/XHR)
  kNetworkingUnfreezableRenderBlockingLoading = 83, // 阻塞渲染的资源加载(优先级高于渲染)
  kJavascriptTimerImmediate = 72,          // setTimeout(fn,0),嵌套层级 < 5
  kJavascriptTimerDelayedHighNesting = 10, // 嵌套层级 >= 5,强制至少 4ms 延迟
  kDatabaseAccess = 16,      // IndexedDB ← 低优先级
  kMicrotask = 9,            // 微任务入口
  kIdleTask = 21,            // requestIdleCallback
  kMainThreadTaskQueueInput = 40, // 输入事件(最高优先级队列)
  // ...共 80+ 种
};

setTimeout(fn, 0) 嵌套层级 < 5 走 kJavascriptTimerImmediate,>= 5 走 kJavascriptTimerDelayedHighNesting 并强制至少 4ms 延迟,这就是深度嵌套 setTimeout(fn, 0) 会变慢的根本原因。

浏览器事件循环的执行顺序

一轮事件循环:
┌─────────────────────────────────────────────────┐
│  1. Blink Scheduler 从最高优先级任务队列取一个任务  │
│  2. 交给 V8 执行(调用栈)                         │
│  3. MicrotaskQueue::PerformCheckpoint()           │  ← 通知 V8 清空微任务
│  4. 执行 requestAnimationFrame 回调               │
│  5. 渲染:Style → Layout → Paint → Composite     │  ← 不是每轮都有
│  6. 回到步骤 1                                    │
└─────────────────────────────────────────────────┘

Blink 如何通知 V8 清空微任务

Blink 通过 WebThread::TaskObserver::DidProcessTask 在每个 Task 结束后调用 blink::Microtask::PerformCheckpoint,即 MicrotaskQueue::PerformCheckpoint(isolate),触发 V8 清空微任务队列。


五、Node.js 的事件循环

Node.js 用 libuv 驱动事件循环,比浏览器多了更细粒度的阶段划分,且额外引入了 process.nextTick 队列。

Node.js 的完整队列体系

每个阶段切换前,Node.js 都会先执行:
  ┌────────────────────────────────────────────────┐
  │  【nextTick 队列】 process.nextTick 回调         │  ← Node.js 独有
  │  【微任务队列】    Promise.then 回调              │  ← V8 维护
  └────────── 两者都清空后,才进入下一阶段 ────────────┘
libuv 事件循环各阶段(uv_run 实际调用顺序):
  1. timers        uv__run_timers()    setTimeout / setInterval 到期回调
  2. pending I/O   uv__run_pending()   上一轮延迟的 I/O 错误回调
  3. idle/prepare  uv__run_idle() / uv__run_prepare()   内部使用
  4. poll          uv__io_poll()       ★ 等待新 I/O 事件(网络响应在此阶段到达)
  5. check         uv__run_check()     setImmediate 回调
  6. close         uv__run_closing_handles()   关闭事件回调

libuv uv_run 真实结构

// deps/uv/src/unix/core.c(精简)
// https://github.com/nodejs/node/blob/main/deps/uv/src/unix/core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);          // 1. timers
    uv__run_pending(loop);         // 2. pending I/O(早期文档常遗漏此步)
    uv__run_idle(loop);            // 3. idle
    uv__run_prepare(loop);         // 3. prepare
    uv__io_poll(loop, timeout);    // 4. poll:阻塞等待 I/O
    uv__metrics_update_idle_time(loop);
    uv__run_check(loop);           // 5. check:setImmediate
    uv__run_closing_handles(loop); // 6. close
  }
  return r;
}
// nextTick/微任务清空 由 Node.js 层(非 libuv)
// 在各阶段回调执行完后通过 processTicksAndRejections() 触发

nextTick 与 Promise 微任务的优先级

// lib/internal/process/task_queues.js
// https://github.com/nodejs/node/blob/main/lib/internal/process/task_queues.js
function processTicksAndRejections() {
  let tock;
  do {
    while ((tock = queue.shift()) !== null) {
      // 先清空 nextTick 队列(AsyncContextFrame 等为现代版本附加字段)
      const asyncId = tock[async_id_symbol];
      emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
      try {
        const callback = tock.callback;
        callback(); // 执行 nextTick 回调
      } finally {
        emitAfter(asyncId);
      }
    }
    // nextTick 全部清空后,触发 V8 清空 Promise 微任务
    runMicrotasks();
  } while (!queue.isEmpty() || processPromiseRejections());
}
// 验证优先级
process.nextTick(() => console.log('1: nextTick'));
Promise.resolve().then(() => console.log('2: Promise'));
process.nextTick(() => console.log('3: nextTick'));
// 输出:1: nextTick → 3: nextTick → 2: Promise

setImmediate vs setTimeout(fn, 0)

// I/O 回调内:setImmediate 稳定先于 setTimeout
// 因为 check 阶段(5)在下一轮 timers(1)之前
fs.readFile('file', () => {
  setTimeout(() => console.log('setTimeout'), 0);
  setImmediate(() => console.log('setImmediate'));
});
// 输出:setImmediate → setTimeout(稳定)
// 主模块顶层:顺序不确定
// 取决于事件循环初始化完成时 timers 是否已到期
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// 输出:不确定

六、浏览器 vs Node.js 对比

维度浏览器Node.js
事件循环驱动Blink Schedulerlibuv
规范依据HTML Living Standard无规范,libuv 实现定义
宏任务队列80+ 种任务源(按优先级)6 个阶段(顺序固定)
微任务队列V8 MicrotaskQueueV8 MicrotaskQueue(同)
额外队列nextTick 队列(优先级高于 Promise)
渲染时机微任务后、下一宏任务前无渲染
触发 V8 微任务DidProcessTask → Microtask::PerformCheckpointprocessTicksAndRejections → runMicrotasks()
setImmediate不支持check 阶段,I/O 后稳定先于 setTimeout
setTimeout(fn,0) 嵌套嵌套 ≥ 5 层强制 4ms同 HTML 规范行为


七、async/await 的本质

// 你写的
async function foo() {
  console.log('A');
  await bar();
  console.log('C');
}
// V8 内部等价(概念示意)
function foo() {
  console.log('A');
  return bar().then(() => {
    console.log('C');  // → EnqueueMicrotask → 微任务队列
  });
}

await 暂停 = 将后续代码通过 TriggerPromiseReactions → EnqueueMicrotask 注册为微任务 await 恢复 = V8 从微任务队列取出,恢复 Generator 继续执行

结论:每个 await 就是一次微任务的入队出队


八、完整执行链路:以 fetch 请求为例

console.log('start');
fetch('/api/data')
  .then(res => res.json())   // cb1
  .then(data => console.log(data)); // cb2
console.log('end');
① 同步执行(调用栈)
   log('start') → fetch() → .then(cb1).then(cb2)【挂在 Promise 上,不在任何队列】
   → log('end') → 调用栈清空
② 网络等待(后台线程,主线程空闲)
   浏览器:Blink 网络线程处理 HTTP
   Node.js:libuv 线程池 / poll 阶段等待
③ 响应到达 → 包装为宏任务入队
   宿主将「resolve Promise」包装为 Task 放入宏任务队列
④ 宏任务执行 → V8
   FulfillPromise() → TriggerPromiseReactions() → EnqueueMicrotask(cb1)
   cb1 进入 V8 微任务队列
⑤ 宏任务结束 → PerformCheckpoint()
   RunMicrotasks: cb1 执行(res.json() 返回新 Promise)
   → EnqueueMicrotask(cb2)
   → RunMicrotasks 继续: cb2 执行(console.log(data))
   → size_ 归零,清空完毕
⑥ cb1/cb2 对象失去引用 → GC 回收

核心认知:回调不是"在队列里等待请求完成",而是请求完成后才被放入队列


九、经典输出题解析

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'));
console.log('5');
// 输出:1 → 5 → 3 → 4 → 2
步骤调用栈V8 微任务队列宿主宏任务队列输出
1log('1')[][]1
2setTimeout[][cb2]-
3Promise.then[cb3][cb2]-
4log('5')[cb3][cb2]5
5栈空 → PerformCheckpoint → cb3[cb4][cb2]3
6RunMicrotasks → cb4[][cb2]4
7size_=0 → 宿主取 cb2[][]2


十、总结

任务调度的本质:两套系统 + 一个接口
  V8:    MicrotaskQueue(环形缓冲区,CSA 内置函数驱动)
                    │
                    │ MicrotaskQueue::PerformCheckpoint()
                    │
  宿主:  宏任务队列
         浏览器 → Blink Scheduler80+ TaskType,多优先级)
         Node.js → libuv 6阶段(timers/pending/idle/poll/check/close)
执行顺序口诀:
  同步代码
    → nextTick(Node.js 独有)
      → 清空微任务(连锁,直到 size_ 归零)
        → 渲染(浏览器)
          → 取下一个宏任务
            → 重复
队列维护者每轮执行量典型 API
调用栈V8全部同步代码函数调用
nextTick 队列Node.js全部清空process.nextTick
微任务队列V8全部清空(连锁)Promise.thenqueueMicrotask
宏任务队列宿主环境每轮取一个setTimeout、I/O 回调

参考源码(全部经过核查)

​转自https://juejin.cn/post/7612218579228360740


该文章在 2026/3/9 15:31:20 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved