同步程式碼跑在 call stack 上。Host 在 stack 之外處理工作。Event loop 協調何時可以跑佇列中的 callbacks。Closures 保留 lexical state。Promises、async/await 與 generators 暫停並恢復工作。
這篇 note 講的是 code 何時執行。class、prototype 與 this 在 TypeScript Class 與 Runtime Identity。Queues 與 5 1 3 4 2 謎題:深入理解 JavaScript Event Loop。
- 同步程式碼跑在 stack 上。
- Host APIs 在 stack 外做完工作並 enqueue 一個 callback。
- 當 stack 為空,loop 排空 microtask queue,然後把 一個 task 推上 stack。
1. Event Loop
瀏覽器每次 iteration 通常會跑 一個 task、排空 所有 microtasks、也許 render,然後再取另一個 task。
log.push("sync start")
setTimeout(() => log.push("macrotask"), 0)
Promise.resolve().then(() => log.push("microtask"))
log.push("sync end")
// ["sync start", "sync end", "microtask", "macrotask"]- JavaScript 不會中斷目前在 stack 上的程式碼。
- Timer 可能在背景結束;它的 callback 要等當前 task 及其 microtasks。
- Failure: 零毫秒的 timer 仍然不是立刻執行。Delay 只是 timer task 的下限。
2. Call Stack
Call stack 是 engine 目前正在執行什麼的 last-in, first-out 記錄。呼叫 function 會 push 一個 frame;return 或 throw 會把它 pop 掉。只有最頂端的 frame 在跑。
function factorialRecursive(n: number): number {
if (n <= 1) return 1
return n * factorialRecursive(n - 1)
}
function factorialIterative(n: number): number {
let result = 1
for (let i = 2; i <= n; i++) result *= i
return result
}- 深層 recursion 每次 call 消耗一個 frame,直到 engine 撞上上限。
- ES2015 規定了 proper tail calls;主流 engines 裡只有 JavaScriptCore 實作了。Recursion depth 仍是實務上限。
- Failure: 緊密的 loop 並不分享 thread。它佔有 thread —— 在它 return 之前沒有 paint、clicks 或 timers。
3. Macrotask Queue
Macrotask 是非正式說法;HTML spec 通常說 tasks。Timers、用戶交互與 messages 會 enqueue 工作。瀏覽器可能維持多條 queues 並 選出 一個可跑的 task,所以「一條全域 FIFO」只是漫畫。
- Loop 跑一個被選出的 task,然後做一次 microtask checkpoint,再跑另一個 task。
- 嵌套的
setTimeout調度的是 未來 的工作。典型順序:timeout-1→timeout-3→timeout-2。 - Failure: 把
setTimeout(fn, 0)當成「下一行之前」。它的意思是「一旦允許跑 timer task」。
4. Microtask Queue
Microtasks —— Promise handlers、queueMicrotask、MutationObserver —— 是 同一條 thread 上優先級更高的 queue。一個 task 結束且 stack 為空之後,runtime 會把它們排空,直到 queue 空掉。
- 一個 microtask 再 queue 另一個 microtask,加進的是 同一次 checkpoint。典型順序:
micro-1→promise→micro-2→macrotask。 - 這就是為什麼 promise handler 會在已經就緒的
setTimeout之前跑。 - Failure: 無界的 microtask chain 會無限推遲 timers、input 與 rendering —— 沒有
while的凍結。
5. Execution Context
Execution context 是 JavaScript 進入 global code 或呼叫 function 時建立的環境:bindings、outer lexical environment,以及 this。
const obj = {
label: "object",
regular(this: { label: string }): string {
const arrow = (): string => this.label
return arrow()
},
}
obj.regular() // "object" — arrow captures the method's `this`- 每次 call 都有自己的 context 與 stack frame。
- Function return 後 frame 被拿掉,但如果回傳的 function close over 了它,lexical environment 仍可觸達。
- Arrow functions 不建立自己的
this;它們從周圍的 context 捕獲。
6. Closures
Closure 不是一種特殊的 function。Function 帶著它被建立時的 lexical environment 的存取權,即使外層 function 已經 return。
function createCounter(start = 0) {
let count = start
return {
inc(): number {
count += 1
return count
},
value(): number {
return count
},
}
}
const counter = createCounter(10)
counter.inc() // 11
counter.value() // 11 — `count` is private to the closure- Private state、event handlers,以及許多 hook patterns 都是 closures。
- 被捕獲的 objects 仍可觸達。Callback close over 一棵大 tree,就可以把它 leak 住。
- Failure: 在 loop 裡用
var建立的 callbacks 共用一個 function-scoped binding。let每次 iteration 建立一個新 binding。Closure 是活的 environment,不是值的副本。
7. Promises
Promise 是 pending,然後變成 fulfilled 或 rejected。呼叫 .then、.catch 或 .finally 並不會立刻跑 handler;settled 的 promise 把它排成 microtask。
function delay<T>(value: T, ms: number): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(value), ms))
}
const user = delay({ name: "Ada" }, 100)
const settings = delay({ theme: "dark" }, 50)
Promise.all([user, settings]).then(([u, s]) => {
// "Ada", "dark" — after ~100 ms, not 150 ms
console.log(u.name, s.theme)
})Promise.all快速失敗。Promise.allSettled記錄每一個 outcome。Promise.race隨第一個結果 settle。- 在 await 它們之前先啟動彼此獨立的 promises,讓底層工作重疊。
- Failure: 把 timer 包進 Promise 並不會讓 timer 變成 microtask。
setTimeout仍然調度一個 task;.then作為 microtask 跑在那個 task 之後。
8. Async/Await
async/await 是 promises 上的語法,不是第二套 concurrency 系統。async function 永遠回傳一個 promise。碰到 await 就 yield;promise settle 之後,其餘部分作為 microtask 繼續。
async function loadDashboard(userId: string) {
const userPromise = getJson<User>(`/api/users/${userId}`)
const settingsPromise = getJson<Settings>("/api/settings")
const [user, settings] = await Promise.all([userPromise, settingsPromise])
return { user, settings }
}await不會擋住 JavaScript thread,也不會讓 JavaScript 變成多執行緒。它暫停的是 那個 async function。連await Promise.resolve()都會 yield。response.json() as Tassertion 描述期望的形狀;它並不驗證。- Failure: 對彼此獨立的 fetches 做順序
await就是 waterfall。一起啟動,再Promise.all。
9. Generators
Generator(function*)被呼叫時並不跑 body。它回傳一個 iterator。每次 .next() 恢復執行,直到下一個 yield 或 function return。
function* range(start: number, end: number): Generator<number> {
for (let i = start; i < end; i++) yield i
}
const values = range(0, 3) // body has not run
values.next() // { value: 0, done: false }
values.next() // { value: 1, done: false }
// 2 is not produced unless another value is requested.- 值只在被請求時才產生 —— lazy sequence,不是一次性的 eventual result。
- Async generator 讓
.next()回傳 promise;for await...of等待每一個結果。這適合 paginated responses 與 streams。 - Failure: 在非 async generator 裡把
yield當成await。Promises 協調一個 eventual value。Generators 協調一連串 pauses。
重點
Call stack 與 execution contexts 解釋 目前 在跑什麼。Task queues、microtasks 與 event loop 解釋延後的工作 何時 可以跑。Closures 解釋為什麼 state 仍可觸達。Promises、async/await 與 generators 組織會暫停與恢復的工作。
當 async 程式碼出乎意料時:目前 stack 上是什麼?什麼被排成 microtask?什麼必須等下一個 task?