任務隊列不是一個,執行順序不是你以為的那樣。本文結合 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 編輯過