JAVASCRIPT V2 · RUNTIME · 26
Event Loop:為什麼 Promise、Timer、Click 的順序不能只看程式碼位置?
42 教你使用 Promise;這一課解釋 Promise continuation 為什麼會在那個時間點執行。核心模型是 call stack → task → microtask checkpoint → render opportunity → next task。
Learning outcomes
- 能畫出 call stack、task queue、microtask queue 的角色。
- 預測 Promise.then / await continuation / setTimeout 的常見順序。
- 理解 microtask checkpoint 為何可能延後 render。
- 能從 long task / microtask starvation 推理 UI 卡頓。
- 知道 Browser event loop 模型與「JavaScript 是不是單執行緒」的精確說法。
1. Why now:async code 能跑,不代表你能預測它
console.log("A");
setTimeout(
() => console.log("B"),
0
);
Promise.resolve()
.then(
() => console.log("C")
);
console.log("D");
如果只靠「從上到下」,你會猜錯。正確答案是 A D C B。
2. Mental model:一輪 event loop
take one task
↓
run JavaScript until stack empty
↓
drain microtasks
↓
browser may render
↓
take next task
這是學習用的最小模型。實際 Browser event loop 規範更細,但這個模型足以解釋大多數前端 ordering bug。
3. Call Stack:目前正在跑什麼
function outer() {
inner();
}
function inner() {
console.log("inside");
}
outer();
stack:
global
↓ outer
↓ inner
↑ return
↑ return
empty
Event loop 不會在任意一行中間把另一個普通 callback 插進 stack。
4. Task:timer、UI event 等待下一輪機會
console.log("A");
setTimeout(
() => console.log("B"),
0
);
console.log("C");
目前 task 先完整輸出 A/C;timer callback 至少等下一個可執行 task opportunity。
5. Microtask:Promise continuation 的優先 checkpoint
console.log("A");
Promise.resolve()
.then(() => console.log("B"));
setTimeout(
() => console.log("C"),
0
);
console.log("D");
// A D B C
同步 stack 清空後,Browser 會先處理 microtasks,再進下一個 timer task。
6. Execution trace:逐步不要跳
| step | stack / queue change | output |
|---|---|---|
| 1 | run console A | A |
| 2 | register Promise continuation → microtask later | — |
| 3 | register timer → task later | — |
| 4 | run console D | D |
| 5 | stack empty → drain microtasks | B |
| 6 | next task | C |
7. await continuation 也是 Promise scheduling
async function demo() {
console.log("inside 1");
await Promise.resolve();
console.log("inside 2");
}
console.log("start");
demo();
console.log("end");
預測:
start
inside 1
end
inside 2
await 前半段同步執行;後半段 continuation 等 Promise settle 後排入 microtask。
8. Worked Example A:microtask 可以新增 microtask
Promise.resolve()
.then(() => {
console.log("A");
queueMicrotask(
() => console.log("B")
);
})
.then(() => {
console.log("C");
});
Microtask queue 在 checkpoint 中持續 drain;新加入的 microtask 也可能在 render/next task 之前執行。
9. Worked Example B:Loading 為什麼畫不出來?
status.textContent =
"Loading";
const end =
performance.now() + 3000;
while (
performance.now() < end
) {
// block main thread
}
status.textContent =
"Done";
雖然 DOM value 曾變成 Loading,但 Browser 可能一直沒有 render opportunity。長同步 task 結束時狀態已變 Done。
10. Worked Example C:microtask starvation
function again() {
queueMicrotask(again);
}
again();
如果 microtask 無限新增自己,checkpoint 永遠 drain 不完,next task 與 render 都可能被餓死。Async 不代表一定 responsive。
11. Browser 與 host environment 的責任
JavaScript engine
→ execute JS / stack / Promise jobs
Browser host
→ timers / network / DOM events / rendering
Event loop
→ coordinate when runnable JS gets a turn
說「JavaScript 是單執行緒」時,最好限定為主要 JS execution stack;Browser 本身當然不只做一件事。
12. Common mistakes
A. 0ms = immediate
錯。至少等 current task 完成。
B. Promise = background thread
Promise 是 future result/scheduling abstraction,不是 worker thread。
C. await 之前與之後都完全 asynchronous
async function 呼叫後會先同步跑到第一個真正 suspend point。
D. 大量 microtask 一定比 task「更快更好」
濫用 microtask 會延後 render 與其他 task。
13. Debug evidence:卡頓要找 long task,不是先怪 API
當 click 有反應但畫面幾秒不更新,可以蒐集:
- Performance timeline 是否出現 long task。
- call stack 最久停在哪個 function。
- Network 是否其實早已完成。
- 是否有 recursion / loop / 大量同步 transform。
如果 Network 50ms 完成、main thread 卻 busy 2500ms,瓶頸不在 server。
14. Guided exercise:預測 ordering
console.log("1");
setTimeout(
() => console.log("2"),
0
);
Promise.resolve()
.then(
() => console.log("3")
);
console.log("4");
要求:先寫 output,再畫 task/microtask queue,再執行驗證。
15. Independent exercise:加入 await
設計一段包含:同步 log、await Promise.resolve()、setTimeout(...,0) 的程式。先預測至少 5 個 log 的順序,再執行。
16. Challenge:兩層 microtask + timer
console.log("A");
Promise.resolve()
.then(() => {
console.log("B");
Promise.resolve()
.then(
() => console.log("C")
);
});
setTimeout(
() => console.log("D"),
0
);
console.log("E");
不要只猜答案;寫出每一步 queue 如何改變。
Project checkpoint:Grade Tracker timing diagram
click task
↓
set loading state
↓
start fetch
↓
stack empty
↓
microtasks
↓
render opportunity
↓
Loading visible
↓
network completes
↓
Promise continuation microtask
↓
state + render results17. Vocabulary / Summary
- call stack:目前正在執行的 function chain。
- task:一輪較高層的 runnable work,例如 timer/event。
- microtask:目前 task 後、下一 task 前處理的 Promise/queueMicrotask work。
- checkpoint:drain microtasks 的時機。
- render opportunity:Browser 有機會更新畫面的時點。
- starvation:某類工作持續佔住排程,使其他工作得不到機會。
18. Further reading
Knowledge check · Mastery
- 預測 sync + Promise + timer 的順序。
- 解釋 await 前後為何分屬不同 execution phase。
- 說明 long task 與 microtask starvation 對 render 的影響。
- 完成頁面下方 Execution Trace mastery。