
結論先講
猜猜這段的輸出順序:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');答案:1, 4, 3, 2。
為什麼?因為 JavaScript 是單執行緒,非同步靠 Event Loop 調度。不同的 async 機制優先級不同:
同步 code > microtask (Promise) > macrotask (setTimeout)
搞懂 Event Loop 你會:
- 不再被
asynccode 的執行順序嚇到 - 知道為什麼長任務會卡 UI
- 知道
requestAnimationFrame什麼時候跑 - 寫出效能更好的 React 元件
基本模型:四個角色
┌───────────────────────────┐
│ Call Stack │ ← 目前在跑什麼
│ ┌─────────────────────┐ │
│ │ doSomething() │ │
│ │ main() │ │
│ └─────────────────────┘ │
└───────────────────────────┘
↑ pop / push
┌───────────────────────────┐
│ Heap(記憶體) │
└───────────────────────────┘
外部(瀏覽器 / Node.js):
┌─────────────┐ ┌──────────────────┐
│ Web APIs │ │ Task Queues │
│ setTimeout │ │ ┌──────────────┐ │
│ fetch │ │ │ Microtask │ │ ← Promise.then
│ DOM events │ │ └──────────────┘ │
│ ... │ │ ┌──────────────┐ │
│ │ │ │ Macrotask │ │ ← setTimeout, I/O
│ │ │ └──────────────┘ │
└─────────────┘ └──────────────────┘
↓ 結果進 queue ↑ Event Loop 挑 task 推進 stack
- Call Stack:目前在執行的函式堆疊
- Web APIs:瀏覽器提供的非同步 API(setTimeout、fetch、DOM 事件)
- Task Queues:等待被執行的任務(分 microtask 跟 macrotask)
- Event Loop:不斷檢查「stack 空了嗎?空了就從 queue 拉 task 來跑」
執行規則:關鍵三條
規則 1:Stack 必須先清空
JS 單執行緒,一次只能跑一個函式。Stack 沒清空前,queue 的 task 都不會被挑。
規則 2:Stack 清空後,先清空 Microtask Queue
Microtask queue 的所有任務跑完才會看 macrotask。
規則 3:Macrotask 一次只跑一個
每跑完一個 macrotask,重新回 step 1(看 stack、再清 microtask)。
回到開頭的例子
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');執行步驟
1. console.log('1') → 印 1(同步)
2. setTimeout(..., 0) → 交給 Web API,0ms 後丟到 macrotask queue
3. Promise.resolve().then() → microtask queue 加一個 task
4. console.log('4') → 印 4(同步)
—— 同步 code 跑完,stack 清空 ——
5. 檢查 microtask queue:有 console.log('3') → 印 3
—— microtask 清空 ——
6. 檢查 macrotask queue:有 console.log('2') → 印 2
輸出:1, 4, 3, 2
關鍵洞察
setTimeout(fn, 0) 不是立即執行,它至少要等到:
- 目前同步 code 跑完
- 所有 microtask 跑完
所以 setTimeout(fn, 0) 實際延遲通常 4ms 以上(瀏覽器最小值)。
Microtask vs Macrotask 分類
Microtask
Promise.then / catch / finallyqueueMicrotask()MutationObserverasync/await(底層就是 Promise)
Macrotask
setTimeout/setInterval- DOM 事件(click、scroll)
setImmediate(Node.js)requestAnimationFrame(特殊 — 看下面)- I/O(fetch 的 response、file read)
關鍵差別
一次 Event Loop tick 會跑完所有 microtask,但 macrotask 只跑一個。
意思是:microtask 可以「插隊」到下一個 macrotask 前面。
setTimeout(() => console.log('macro 1'), 0);
setTimeout(() => console.log('macro 2'), 0);
Promise.resolve().then(() => {
console.log('micro 1');
Promise.resolve().then(() => console.log('micro 2')); // 仍在這輪
});輸出:micro 1, micro 2, macro 1, macro 2
即使 micro 2 是 micro 1 跑到一半才加的,它還是在 macro 1 之前跑。
為什麼長任務會卡 UI
UI 更新(render、paint)也在 Event Loop 裡跑。一個流程:
同步 code
↓
清空 microtask
↓
(瀏覽器可選)render / paint
↓
跑一個 macrotask
↓
回 step 1
如果某個同步任務跑太久(例如大量迴圈),整個流程卡住,UI 也更新不了。
function blockingLoop() {
const start = Date.now();
while (Date.now() - start < 3000) {
// 忙轉 3 秒
}
}
button.addEventListener('click', () => {
blockingLoop(); // UI 凍結 3 秒
});解法:拆開跑
async function nonBlocking() {
for (let i = 0; i < 1000000; i++) {
if (i % 10000 === 0) {
await new Promise(r => setTimeout(r, 0)); // 讓 Event Loop 喘息
}
// ... do work
}
}或用 Web Worker 搬到另一個執行緒。
requestAnimationFrame:特殊的 macrotask
rAF 的 callback 在下次 repaint 之前跑,跟 macrotask 類似但不在一般 queue。
requestAnimationFrame(() => {
// 瀏覽器要畫面更新前執行
});用途:動畫。瀏覽器能保證 60fps(每 16.7ms 一幀)時 rAF 每幀跑一次。
不要用 setTimeout 做動畫:setTimeout 跟 paint 沒同步,會掉幀。用 rAF。
async/await 的 Event Loop 行為
async/await 表面看起來同步,底層就是 Promise(microtask)。
console.log('A');
async function foo() {
console.log('B');
await Promise.resolve();
console.log('C'); // microtask
}
foo();
console.log('D');輸出:A, B, D, C
await 把後面的程式變成 microtask,所以 C 晚於 D。
React 的 Event Loop 運用
React 18 的 automatic batching:
function handleClick() {
setState1(1);
setState2(2);
setState3(3);
}React 把這三個 setState 合併成一次 render(microtask),而不是 3 次。這讓效能提升。
自動 batching 的前提是 React 能控制這個 task boundary。await 之後就跳出 batching:
async function handleClick() {
setState1(1);
await fetch('/api');
setState2(2); // 另一個 render(不同 microtask)
}Debug 工具:DevTools Performance 面板
Chrome DevTools Performance tab 可以錄一段時間看:
- 每個 task 多長
- Long Task(> 50ms 警告)
- Microtask、Animation Frame、Paint 的時序
- 什麼時候卡住了
效能優化必備工具。
實戰 Checklist
- 知道 microtask 比 macrotask 優先
-
setTimeout(fn, 0)不是立即執行 - 長任務拆成小段避免卡 UI(用 await setTimeout)
- 動畫用
requestAnimationFrame,不用setTimeout - React state update 之後用
await會跳出 batching - Long Task > 50ms 要警覺
- Debug 用 Performance 面板看 task 時序
相關文章
- Scope 與 Closure — 前一篇
- JS 子 Roadmap
- 下一篇 → Promise / async-await 錯誤處理(🌱)
- Frontend Roadmap
