任务队列不是一个,执行顺序不是你以为的那样。本文结合 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:
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:
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% 的性能提升):
int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {
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
enum class MicrotasksPolicy {
kExplicit,
kScoped,
kAuto
};
V8 暴露给宿主的触发入口是 MicrotaskQueue::PerformCheckpoint(v8::Isolate*),宿主每完成一个任务,就调用它触发微任务清空。
三、Promise 与微任务的关联
.then() 的回调为什么是微任务?真实的调用链:
Promise.resolve()
→ FulfillPromise() ← 修改 Promise 状态
→ TriggerPromiseReactions() ← 触发所有 .then 回调
→ EnqueueMicrotask() ← ★ 真正入队微任务
入队发生在 promise-abstract-operations.tq:
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 真实结构
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);
uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
uv__io_poll(loop, timeout);
uv__metrics_update_idle_time(loop);
uv__run_check(loop);
uv__run_closing_handles(loop);
}
return r;
}
nextTick 与 Promise 微任务的优先级
function processTicksAndRejections() {
let tock;
do {
while ((tock = queue.shift()) !== null) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
try {
const callback = tock.callback;
callback();
} finally {
emitAfter(asyncId);
}
}
runMicrotasks();
} while (!queue.isEmpty() || processPromiseRejections());
}
process.nextTick(() => console.log('1: nextTick'));
Promise.resolve().then(() => console.log('2: Promise'));
process.nextTick(() => console.log('3: nextTick'));
setImmediate vs setTimeout(fn, 0)
fs.readFile('file', () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
六、浏览器 vs Node.js 对比
| 维度 | 浏览器 | Node.js |
|---|
| 事件循环驱动 | Blink Scheduler | libuv |
| 规范依据 | HTML Living Standard | 无规范,libuv 实现定义 |
| 宏任务队列 | 80+ 种任务源(按优先级) | 6 个阶段(顺序固定) |
| 微任务队列 | V8 MicrotaskQueue | V8 MicrotaskQueue(同) |
| 额外队列 | 无 | nextTick 队列(优先级高于 Promise) |
| 渲染时机 | 微任务后、下一宏任务前 | 无渲染 |
| 触发 V8 微任务 | DidProcessTask → Microtask::PerformCheckpoint | processTicksAndRejections → 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');
}
function foo() {
console.log('A');
return bar().then(() => {
console.log('C');
});
}
await 暂停 = 将后续代码通过 TriggerPromiseReactions → EnqueueMicrotask 注册为微任务 await 恢复 = V8 从微任务队列取出,恢复 Generator 继续执行
结论:每个 await 就是一次微任务的入队与出队。
八、完整执行链路:以 fetch 请求为例
console.log('start');
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data));
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');
| 步骤 | 调用栈 | V8 微任务队列 | 宿主宏任务队列 | 输出 |
|---|
| 1 | log('1') | [] | [] | 1 |
| 2 | setTimeout | [] | [cb2] | - |
| 3 | Promise.then | [cb3] | [cb2] | - |
| 4 | log('5') | [cb3] | [cb2] | 5 |
| 5 | 栈空 → PerformCheckpoint → cb3 | [cb4] | [cb2] | 3 |
| 6 | RunMicrotasks → cb4 | [] | [cb2] | 4 |
| 7 | size_=0 → 宿主取 cb2 | [] | [] | 2 |
十、总结
任务调度的本质:两套系统 + 一个接口
V8: MicrotaskQueue(环形缓冲区,CSA 内置函数驱动)
│
│ MicrotaskQueue::PerformCheckpoint()
│
宿主: 宏任务队列
浏览器 → Blink Scheduler(80+ TaskType,多优先级)
Node.js → libuv 6阶段(timers/pending/idle/poll/check/close)
执行顺序口诀:
同步代码
→ nextTick(Node.js 独有)
→ 清空微任务(连锁,直到 size_ 归零)
→ 渲染(浏览器)
→ 取下一个宏任务
→ 重复
| 队列 | 维护者 | 每轮执行量 | 典型 API |
|---|
| 调用栈 | V8 | 全部同步代码 | 函数调用 |
| nextTick 队列 | Node.js | 全部清空 | process.nextTick |
| 微任务队列 | V8 | 全部清空(连锁) | Promise.then、queueMicrotask |
| 宏任务队列 | 宿主环境 | 每轮取一个 | setTimeout、I/O 回调 |
参考源码(全部经过核查)
转自https://juejin.cn/post/7612218579228360740
该文章在 2026/3/9 15:31:20 编辑过