JavaScript 的非同步執行模型:call stack、web APIs、task queue、microtask queue 與 event loop
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(12)
1. Outline
- 起點 · JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
從 runtime 全景圖出發,先建立單一 call stack 的執行模型,再用長任務逼出問題,一路推出 web APIs、task queue、microtask queue 與 event loop 的分工,最後用一道 51342 的測驗驗收。
2. YouTuber 的思維推導
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
Lydia Hallie · 12m34s · 字幕 en · vision=on (整支影片是動畫講解,作者不斷指著畫面上的 call stack、queue、promise 物件說明搬移過程;程式碼與執行順序都畫在投影片上,不看圖無法對應說明。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 2s |
| segment | 55s | – |
| shot | 29s | 6s |
| analyze | 5m00s | 9m15s |
| render | 5s | – |
作者從一個定位動作出發:event loop 名氣很大,但它其實只是 JavaScript runtime 裡的一個小零件,要懂它必須先看整張圖。於是她先把 runtime 拆開(JS engine 裡的 call stack 與 memory heap、web APIs、task queue、microtask queue、event loop),再從最基本的 call stack 開始:JavaScript 單執行緒、一次只能跑一件事,執行順序完全由堆疊的推入與彈出決定。接著她故意讓這個模型撞牆——放一個重運算的長任務進去,整個頁面凍結;而真實應用又非用長任務不可(網路請求、使用者輸入、計時器)。
這個矛盾就是整支影片要解的題。她的答案分三步:第一步,長任務其實不由 JavaScript 自己跑,而是透過 web APIs 卸載給瀏覽器,發起呼叫雖然會上 call stack,但只是登記 callback 與啟動背景工作,登記完立刻彈出;第二步,工作完成後的 callback 不能直接插隊回 call stack(會打斷正在跑的任務),所以先進 task queue 排隊,由 event loop 在 call stack 空掉時搬過去——這就補上了 event loop 的角色;第三步,promise 型 API 走另一條佇列 microtask queue,而且 event loop 給它優先權,必須整個清空才輪到 task queue,且每處理完一個 task 又回頭再檢查一次。規則到齊後她用 setTimeout 拆掉一個常見誤解(延遲是到進入 task queue 為止,不是到執行為止),再用一題 51342 的小測驗把所有規則同時驗證一次,最後用一段 recap 把整條鏈重述,並提醒讀者自己動手玩 setTimeout 與 queueMicrotask 才會變成直覺。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- Event Loop 只是 runtime 的一個小零件 → 全景圖上唯一「正在執行你的程式碼」的格子只有 call stack,其他格子都還是空的。所以下一步必須先搞清楚:這個唯一的堆疊,是怎麼決定一支程式的執行順序的?
- 單一 call stack 怎麼跑完一支程式 → 這個模型有一個沒被檢驗的前提:每個 execution context 都會「很快」彈出去。萬一有一個彈不掉呢?下一段就故意放一個彈不掉的東西進去。
- 單執行緒的代價:長任務會凍結畫面 → 作者自問「是不是就被鎖死了」,並且自答「不會,因為這些情況我們用的是 web APIs」。所以下一段必須先回答:web APIs 到底是什麼,它憑什麼能讓等待不占用 call stack?
- Web APIs:把長任務交給瀏覽器 → 作者在這一段結尾把有非同步能力的 web API 分成 callback 型與 promise 型兩類,並宣告先看 callback 型。所以下一段要用一個具體的 callback 型 API,把「登記 → 背景執行 → 結果回來」整條流程走一遍。
- Callback 型 API:為什麼不能直接放回堆疊 → 資料已經回來、callback 卻懸在半空,call stack 也空著,畫面上兩個佇列還是空的。下一段必須回答:這個 callback 到底先放到哪裡,又是誰決定什麼時候把它搬上 call stack?
- Task queue 與 event loop 的分工 → 這條回程路是用 geolocation 走通的,但 geolocation 只是眾多 callback 型 API 之一。下一段換上每個人天天在用的 setTimeout,用同一套規則檢驗它——而且會發現一個關於「延遲」的普遍誤解。
- setTimeout:延遲是到佇列,不是到執行 → 作者在這一段結尾自己收束了 callback 型的結論(web API 的 callback 在非同步任務完成時被推進 task queue),然後直接拋出問題:那 promise 型的呢?下一段要走的是另一條佇列。
- Microtask queue 有優先權 → 兩條路都走過了:callback 型進 task queue、promise 型進 microtask queue,而且後者有優先權。既然 promise 型明顯好用,下一段順手處理一個實務問題——手上那些舊的 callback 型 API 能不能改寫成 promise 型?
- 把 callback 型 API 包成 promise → 至此 call stack、web APIs、task queue、microtask queue、event loop 五個零件與它們的規則都齊了。下一段作者要用一道題,把這些規則放在同一支腳本裡同時檢驗。
- 小測驗:為什麼答案是 51342 → 推導在真實情境下成立了。剩下的只差把這一路走過的零件與規則,用一段話重新串成一條完整的鏈。
- 整條路線重講一次 → 規則與零件都收攏完了,唯一還沒解決的是最後一哩:怎麼把這套規則從「聽得懂」變成「用得出來」。
- 自己動手試才會真的懂
3. 逐段說明
JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue
1. Event Loop 只是 runtime 的一個小零件 0:00–0:32
作者先把鏡頭拉遠:event loop 名聲很大,但它只是 JavaScript runtime 裡的一個小元件。runtime 還有 call stack(嚴格說是 JS engine 裡的 call stack 與 memory heap)、web APIs、task queue、microtask queue。這些零件合起來,才讓 JavaScript 能以非阻塞的方式處理非同步工作。

推理因為讀者對 event loop 的印象是「一個難懂又神秘的大東西」,所以作者刻意先不解釋它,而是把鏡頭拉遠:先畫出整個 JavaScript runtime 的全景(call stack、web APIs、task queue、microtask queue,最後才是 event loop),讓讀者看見 event loop 只是其中一格。她甚至補了一句技術上更精確的說法——call stack 與 memory heap 其實在 JavaScript engine 裡面,只是為了投影片乾淨才只畫 call stack。她的目的是:先給地圖,後面每一段才知道自己站在哪一格。
AI 補充這裡有一個作者為了節奏而快速帶過、但影響後面全部推理的區分:**engine 不等於 runtime**。engine(V8、SpiderMonkey)只負責解析與執行 JavaScript,它裡面有 call stack 和 memory heap;而 web APIs、task queue、microtask queue、event loop 這幾格**都不在 engine 裡**,是宿主環境(瀏覽器,或 Node.js)額外提供的。這解釋了兩件後面會用到的事:第一,setTimeout、fetch 這些其實不是 JavaScript 語言的一部分,在 ECMAScript 規格裡找不到;第二,同一份 JavaScript 換到 Node.js 跑,engine 一樣但周邊零件換了一套,所以行為才會有差異。畫面上的全景圖也已經暗示了資料流向:web APIs 在右上、兩個 queue 在下方、event loop 站在 queue 左邊像個搬運工——東西是從右上繞一圈回到左上的 call stack,而不是直接回去。
2. 單一 call stack 怎麼跑完一支程式 0:32–1:18
因為 JavaScript 是單執行緒、只有一個 call stack,所以程式的執行順序完全由這個堆疊決定。作者用一支有巢狀函式呼叫的腳本示範:每次呼叫就建立 execution context 推上堆疊,執行完就彈出,巢狀呼叫則層層堆高再層層退回。

推理因為上一段已經指出 call stack 是唯一的執行現場,所以作者接著給出限制條件——JavaScript 是單執行緒,只有一個 call stack——再用一支最單純、完全沒有非同步的腳本示範:每次函式呼叫建立一個 execution context 推上堆疊,執行完彈出;巢狀呼叫就層層堆高、再層層退回。她要先讓讀者建立「執行順序 = 堆疊的推入彈出順序」這條直覺,之後才有東西可以被打破。
AI 補充這一段的畫面比旁白多說了一件事,值得停下來看:截圖停在第 11 行(console.log("Four!"))時,call stack 上只剩 logThreeAndFour() 一層,console 已經印出 One、Two、Three。也就是說 logThree 與它裡面的 console.log 都已經彈掉了——巢狀呼叫堆得再高,也是「先進後出」一路退回來,堆疊的深度隨時在變。這裡補一步作者跳過的:最底下其實還有一個沒畫出來的全域 execution context(規格上是最外層的 script 執行),你的 console.log("One!") 是在它裡面跑的。理解這點,下一段「整個程式凍結」才說得通——凍結的意思正是:這個最底層的 context 遲遲彈不掉,堆疊永遠不會空。
3. 單執行緒的代價:長任務會凍結畫面 1:18–2:01
既然一次只能處理一件事,一個很重的運算就會卡住後面所有程式碼,整個頁面在那幾秒鐘完全凍結。但現實應用一定會遇到長任務——網路請求、等使用者輸入、計時器。作者在這裡丟出問題:那 call stack 是不是就被鎖死到資料回來為止?

推理因為上一段建立的模型是「順序完全由堆疊決定、一次一件」,所以作者接著推出它的必然後果:堆疊上那一格只要待得夠久,後面所有程式碼都得排隊,整個頁面在那幾秒完全沒反應。但她沒有停在「所以不要寫長任務」——她立刻補上現實:網路請求、等使用者輸入、計時器,這些長任務躲不掉。於是她把矛盾攤開,並用一個提問結束這一段:那 call stack 是不是就被鎖死到資料回來為止?這是整支影片真正的題目。
AI 補充截圖把代價畫得比旁白更清楚:call stack 上只有 longRunningTask() 一格、右上角轉圈,console 區塊完全空白——注意連第 6 行自己的 console.log("Long task done!") 都還沒印出來,第 14 行的 importantTask() 更是連被呼叫的機會都沒有。這裡要補一個作者沒說、但決定嚴重性的關鍵:被擋住的不只是你的 JavaScript。瀏覽器的算繪、捲動、點擊回應,跟你的程式碼共用同一條主執行緒,所以「凍結」是字面意義上的整個分頁不動,不是只有 console 慢一拍。另外要分清楚兩種長任務:這裡的重運算是**真的在 call stack 上算**,卸載給誰都沒用(那要靠 Web Worker);而下一段要處理的網路請求、計時器則是**在等別人**,等待這件事根本不需要占著堆疊。作者接下來的解法只解後者,這條界線她沒有明講。
術語:blockinglong-running task
4. Web APIs:把長任務交給瀏覽器 2:01–2:47
答案是不會,因為這些情境用的是 web APIs。web APIs 是瀏覽器提供的一組介面,讓我們使用瀏覽器的能力:DOM、fetch、setTimeout,還有感測器、相機、地理位置等等。其中一部分 web API 允許我們把長任務「卸載」給瀏覽器處理;呼叫它們時,我們只是啟動這個卸載動作。而具備非同步能力的 web API 分成兩類:callback 型與 promise 型。

推理因為上一段留下的解法只是一個名詞,所以作者接著補上它的定義與範圍:web APIs 是瀏覽器提供的一組介面,讓 JavaScript 能使用瀏覽器的能力——常用的 DOM、fetch、setTimeout,還有感測器、相機、地理位置這些比較有趣的。鋪完範圍她才切回主線:其中一部分 web API 允許我們把長任務卸載給瀏覽器,而呼叫它們時,我們做的只是「發動這個卸載」。最後她把有非同步能力的 web API 分成兩類(callback 型與 promise 型),替後面兩條路線先分好岔口。
- CWeb API:瀏覽器提供給 JS 用的介面,不屬於 JS 語言本身,不全是非同步的→ 畫地圖
- C卸載:發起呼叫仍上 call stack 但只做登記交接,真正工作在瀏覽器背景跑,不占堆疊→ 畫地圖
- A非同步 Web API 呼叫像轉一下門把,登記完立刻放手→ 批判類比
AI 補充這張圖是本段的重點,因為它把上一段的困境從物理層面解掉了:畫面把 JAVASCRIPT RUNTIME 整個框起來放在左邊,右邊另一個框是 BROWSER ENVIRONMENT,裡面是 Rendering Engines、Network Service、GPU、Sensors、Camera、Geolocation,中間用一對左右箭頭相連。也就是說——瀏覽器本身是多執行緒的,網路請求真的在另一條線上跑。web API 只是那條界線上的門把:你在 call stack 上轉一下門把(登記工作),實際的活由門後的瀏覽器去做,你的堆疊立刻恢復空閒。這正好接上前一段的分類:這套機制對「在等別人」的長任務有效,對「自己在算」的重運算無效,因為 1e9 次迴圈沒有任何門後的服務可以代勞。另外提醒一個容易混淆的邊界:像 URL、localStorage 這些也在 web API 清單裡,但它們是同步的,一樣會擋住 call stack——「是 web API」不等於「是非同步的」,只有一部分 web API 具備非同步能力。
5. Callback 型 API:為什麼不能直接放回堆疊 2:47–4:04
作者用 geolocation 的 getCurrentPosition 示範:呼叫時它確實被推上 call stack,但那只是為了登記 success 與 error 兩個 callback、啟動背景工作,登記完立刻彈出,不等資料。瀏覽器在背景跑流程、跳出授權視窗,這期間網站照常有反應。等使用者按下允許、資料回來,callback 卻不能直接被推回 call stack——那會打斷正在執行的任務,行為變得無法預測。


推理因為上一段說「呼叫非同步 web API 只是發動卸載」,所以作者接著逐格驗證這句話:getCurrentPosition 的呼叫確實被推上 call stack,但那只是為了登記 success 與 error 兩個 callback、啟動背景任務,登記完立刻彈出,不等任何資料。她刻意強調中間那段最長的空白——瀏覽器跳出授權視窗,使用者可能十秒後才按,但這件事不發生在 call stack 上,所以網站照常有反應。最後使用者按下允許、資料回來,她卻沒有讓 callback 直接回到堆疊,而是停在那裡指出:不能這樣做,那會打斷正在執行的任務,造成無法預測的行為。這個「停住不做」是刻意的,因為它就是下一段那個機制存在的理由。
AI 補充兩張截圖剛好是這段的前後兩個關鍵時刻。第一張(登記那一刻)值得細看:Web APIs 框裡的 Geolocation API 底下,getCurrentPosition 已經把 successCallback 與 errorCallback 兩個函式整個存了起來,而 call stack 上還留著 getCurrentPosition() ——這一格馬上就會消失,兩個 callback 卻會留在門後等。第二張(使用者按允許那一刻)則是 call stack 完全空、task queue 與 microtask queue 也都還空著。這裡補一個作者沒明講、但決定整個設計的理由:callback 之所以不能直接插隊,是因為 JavaScript 保證**執行到完為止**(run-to-completion)——一個函式一旦開始跑,就不會跑到一半被別的程式碼插進來,你不必為每個變數加鎖。如果讓 callback 想回來就回來,這個保證立刻破功:你讀完 obj.a、正要讀 obj.b 之間,值可能已經被改掉。所以非同步的結果只能等目前這一輪完全跑完,這個限制不是實作偷懶,而是整個語言不需要鎖的前提。另外提醒:登記的兩個 callback 只會有一個被呼叫,且要是使用者一直不理那個視窗,callback 就永遠不會回來——非同步不保證會發生。
術語:callback-based API
6. Task queue 與 event loop 的分工 4:04–4:39
所以 callback 改成被推進 task queue(也叫 callback queue),這裡排隊的是 web API 的 callback 與事件處理函式,等著稍後被執行。event loop 的職責就是盯著 call stack:一旦它空了,就從 task queue 取出第一個任務搬上 call stack 執行。使用者的位置就是在這一刻才被印出來。

推理因為上一段已經論證「插隊會破壞正在執行的任務」,所以作者接著給出唯一不破壞的做法:讓 callback 先進 task queue 排隊——她順帶解釋了它為什麼也叫 callback queue,因為裡面放的正是 web API 的 callback 與事件處理函式。有了排隊的地方,還缺一個判斷「現在可以搬了嗎」的角色,於是 event loop 終於登場,而它的職責被壓縮成一句非常小的話:檢查 call stack 是不是空的;空了就從 task queue 取第一個任務搬上去。使用者的位置就在這一刻才被印出來——這是整支影片第一次完整跑完一條非同步回程路。
- CTask queue:存放 web API callback 與事件處理函式的先進先出佇列→ 畫地圖
- CEvent loop:不執行程式碼,只不斷檢查 call stack 是否為空,空了就搬一個任務上去→ 畫地圖
- R卡住時狂點按鈕為什麼沒反應→ 存+回想
AI 補充截圖把這句定義畫成三格:call stack 空、Web APIs 空、task queue 裡躺著一個 Task。整段最容易被高估的就是 event loop 本身——看完這張圖應該建立的印象是:它不執行任何程式碼、不排程、不決定優先順序,只是一個不停問「堆疊空了嗎」的迴圈,空了就搬一個過去。名氣那麼大的東西其實只有這麼一條規則,這也回應了第 1 段「它只是一個小零件」的開場。這裡要補兩個作者沒說、但實務上常撞到的細節。第一,「call stack 空了」指的是目前這一整支同步程式碼全部跑完,不是某個函式回傳——所以你不可能在函式中間讓 callback 插進來,這正是上一段 run-to-completion 保證的另一面。第二,佇列是先進先出的,所以兩個先後完成的非同步工作,callback 也會照完成順序執行;但「完成順序」由瀏覽器背景決定,不等於你呼叫的順序,兩個 fetch 誰先回來並無保證。
7. setTimeout:延遲是到佇列,不是到執行 4:39–6:22
另一個常見的 callback 型 API 是 setTimeout。它被推上 call stack 也只是把 callback 與延遲登記給 timers API,計時交給瀏覽器。時間到了,callback 進 task queue,call stack 空了才被搬上去執行。所以最關鍵的一句是:你指定的延遲,是到「進入 task queue」為止的延遲,不是到執行為止;如果 call stack 還很滿,callback 得繼續排隊。

推理因為上一段的規則是用一個大家不常寫的 API 建立的,所以作者接著用最常見的 setTimeout 再跑一次,證明規則通用:setTimeout 被推上 call stack 也只是把 callback 與延遲登記給 timers API,計時交給瀏覽器,自己立刻彈出;兩個計時器都登記完,同步的 console.log 照常先印。時間一到,瀏覽器把 callback 丟進 task queue,call stack 空了才被搬上去。跑完這一輪,她才點出真正想講的那句話——你指定的延遲,是到「進入 task queue」為止的延遲,不是到執行為止。這句話之所以能成立,完全是靠前一段建立的規則推出來的,不是額外的新規定。
AI 補充截圖抓在最能說明問題的一格:console 已經印出「End of script!」,call stack 上是 100ms 的 callback,而 Web APIs 框裡 setTimeout 底下還留著 2000 的 delay 在倒數。注意兩件事:第一,程式碼裡 2000 那個 setTimeout 寫在前面,卻是 100 那個先執行——順序由「誰先到期」決定,不是誰先寫;第二,同步的 console.log 永遠贏過任何 setTimeout,就算延遲寫 0 也一樣。這裡補上作者省略、但實務常撞到的三點:其一,setTimeout(fn, 0) 不是立刻執行,它仍要走完整條回程路,而且 HTML 規格規定巢狀超過 5 層時延遲會被夾到至少 4 毫秒;其二,背景分頁的計時器會被瀏覽器降頻到一秒一次以省電;其三,「延遲到期」與「開始執行」之間隔了多久,取決於 task queue 前面排了什麼、以及 call stack 上正在跑什麼——所以第 3 段那個重運算長任務,會讓你所有的計時器一起遲到。這也說明了為什麼 setTimeout 不能拿來做精準計時。
8. Microtask queue 有優先權 6:22–8:40
promise 型 API 走的是另一條路:microtask queue,專門收 then/catch/finally 的 callback、await 之後的函式主體、queueMicrotask 與 MutationObserver 的 callback。event loop 會優先清空 microtask queue,全部搬完才碰 task queue,而且處理完 task queue 的每一個任務後又回頭檢查一次。作者用 fetch 走一遍:建立 pending 的 promise、登記 reaction、同步的 log 先印,資料回來才把 handler 推進 microtask queue。附帶陷阱:microtask 可以再排 microtask,無限下去就永遠輪不到 task queue,程式一樣凍結。


推理因為上一段已經把 callback 型的回程路走到底,所以作者接著補上另一條路,並且先給規則、後給示範。規則有兩條:第一,microtask queue 是專用的,只收 then/catch/finally 的 callback、await 之後的函式主體、queueMicrotask 與 MutationObserver 的 callback;第二,event loop 對它有優先權——call stack 一空,先把 microtask queue **整個清空**,才碰 task queue,而且每處理完 task queue 的一個任務又回頭再檢查一次。接著她用 fetch 走一遍驗證:呼叫 fetch 建立一個 pending 的 promise 並發動背景請求、.then 建立 promise reaction record、同步的 console.log 先印、伺服器回來才把 handler 推進 microtask queue。最後她補上這條優先權的代價:microtask 可以再排 microtask,無限下去就永遠輪不到 task queue,程式一樣凍結。
- CMicrotask queue:優先權高於 task queue,call stack 一空就要整個清空才碰 task queue→ 畫地圖
- Rfetch 呼叫時 call stack 上發生什麼→ 存+回想
- R.then 本身是同步的→ 存+回想
- RqueueMicrotask 跟 MutationObserver 走哪條佇列→ 存+回想
- C微任務餓死:microtask 不斷排入新的 microtask,佇列永遠清不空,task queue 永遠輪不到→ 畫地圖
AI 補充兩張截圖分別對應規則與示範。第一張把優先權畫得很清楚:call stack 上正在跑一個 Task,task queue 裡還排著兩個 Task,而 microtask queue 是空的——順序不是「誰先排隊誰先跑」,而是「先看 microtask queue 有沒有東西」。第二張則把 promise 的內部狀態攤開給你看:[[PromiseState]] 是 fulfilled、[[PromiseResult]] 是 Response、[[PromiseFulfillReactions]] 裡裝著 res => console.log(res)。這張圖回答了一個 promise 教學常跳過的問題——.then 到底做了什麼?它不是「等結果」,而是把 handler 登記進 promise 物件的 reactions 清單;promise 一旦落定,才把清單裡的 handler 排進 microtask queue。所以 .then 是同步執行的,handler 才是非同步的。這裡補一個作者沒點破、但能把兩條規則接起來的觀點:優先權的設計目的是讓「同一件事情的後續處理」不要被使用者的點擊或計時器插隊,也就是讓 promise 鏈看起來像是連續發生的。代價就是她提到的餓死問題——因為規則是清空而不是取一個,一個會自我複製的 microtask 可以讓 task queue 永遠等不到,畫面也永遠不更新。順帶補一個順序上的常見考題:同時有 setTimeout(fn, 0) 和 Promise.resolve().then(fn),永遠是 promise 的先跑,理由就在這條優先權,不在延遲時間。
術語:microtask starvation
「I believe in node you can set like Mex tick depth or something like that which prevents this exact thing from happening」
「so only those callbacks or those function body parts get pushed onto the microtask CU so it's very specific」
9. 把 callback 型 API 包成 promise 8:40–8:57
既然兩條路都走過一遍,作者順手示範把 callback 型 API promisify:用 new Promise 包住 getCurrentPosition,把 resolve 與 reject 當成 success 與 error callback 傳進去,程式碼可讀性會好一些。
推理因為兩條路都走過、也看出 promise 型的優點,所以作者順手示範把 callback 型 API promisify:用 new Promise 包住 getCurrentPosition,把建構子給的 resolve 與 reject 直接當成 success callback 與 error callback 傳進去。她強調這只是可讀性的改善——這個說法很誠實,因為底層機制完全沒變。
AI 補充這段短到容易被跳過,但它其實驗證了前面兩段建立的模型。包裝之後,底下跑的還是同一個 getCurrentPosition、同一個瀏覽器背景流程;唯一改變的是**結果的回程路**:原本 success callback 由 web API 直接排進 task queue,現在被排進 task queue 的是 resolve 這個動作,resolve 讓 promise 落定,落定才把你的 .then handler 排進 microtask queue。也就是說 promisify 之後多繞了一層,實際執行時機反而比原本晚一點點(多一次佇列跳轉),只是慢的量級小到無關緊要。要補充的是為什麼「可讀性」值得這一層:包成 promise 之後才能用 await 串起來、才能用 try/catch 接錯誤、才能丟進 Promise.all 一起等,而這些都是 callback 型 API 做不到的。另外提醒兩個包裝時的陷阱:new Promise 的執行器函式是**同步**執行的,所以裡面的錯誤要記得走 reject 而不是 throw;以及 resolve 只有第一次呼叫算數,之後再呼叫沒有作用。
術語:Promise constructor
10. 小測驗:為什麼答案是 51342 8:57–10:52
作者出一題:resolved promise 加 then、setTimeout、巢狀的 queueMicrotask,最後一行同步 console.log 5,問印出順序。答案是 5、1、3、4、2。理由完全來自前面的規則:同步的 5 先印;call stack 空了先清 microtask queue,所以 1、3 依序出來;第三層 queueMicrotask 排進來時 microtask queue 還沒清空,所以 4 也要先處理完;最後才輪到 task queue 裡的 setTimeout callback 印出 2。


推理因為前面每條規則都是分開建立的,所以作者接著設計一個把它們疊在一起的情境,逼讀者自己排序。答案是 5、1、3、4、2,而每一位數字都對應一條已經講過的規則:5 先印,因為同步程式碼永遠先跑完(第 2 段);1 接著,因為 promise 已經 resolved,handler 早在 microtask queue 裡,而 call stack 一空就先清 microtask queue(第 8 段);3 是同一輪 microtask queue 裡的下一個;4 是陷阱所在——它是在清空過程中才被排進來的 microtask,而規則是「整個清空」,所以它也算這一輪;2 最後,因為 setTimeout 的 callback 在 task queue,必須等 microtask queue 完全清空才輪到(第 8 段)。整題沒有用到任何新規則。
AI 補充兩張截圖剛好夾住這題的陷阱。第一張是題目本身:注意第 4 行的 setTimeout 延遲寫 10 毫秒,而整支腳本跑完根本不用 1 毫秒——所以 2 排在最後,跟延遲長短其實沒有關係,就算寫 0 也一樣。第二張是關鍵那一格:console 已有 5、1、3,call stack 上是 console.log(4),而 task queue 裡那個 console.log(2) 還躺著沒動。這一格證明的正是「清空」與「取一個」的差別——第 8 行那個巢狀 queueMicrotask 是在 microtask queue 正在被清空的過程中才排進來的,如果規則是每輪取一個,4 就會排到 2 後面。這裡補一個作者沒有明說的觀念:event loop 在每個 task 之間執行的這次「把 microtask queue 徹底清空」的動作,規格上叫 microtask checkpoint,它才是這一題的判準。實務上推這類題目有個穩定的順序:先把同步碼一路跑完(5),再清空 microtask(1、3、4,包含中途新增的),最後才拿一個 task(2),拿完再回頭做一次 checkpoint。作者說「先暫停影片自己想」是有道理的——能自己推出 4 在 2 前面,代表你真的接受了優先權規則,而不是背下順序。
11. 整條路線重講一次 10:52–11:58
收尾的複習把整條鏈串起來:JavaScript 單執行緒、一次一個任務;web APIs 讓我們用瀏覽器的能力,其中一些能把非同步任務丟到背景;發起呼叫仍會上 call stack,但只是交接,真正的工作在背景跑,不擋堆疊;callback 型 API 完成後把 callback 排進 task queue;promise 相關的則進 microtask queue,它有優先權,而且每處理完一個 task 就再檢查一次。
推理因為前面是一段一段推上來的,所以作者接著把同一條鏈倒過來快速重述一次,讓讀者檢查自己的模型有沒有缺角:JavaScript 單執行緒、一次一個任務;web APIs 讓我們用瀏覽器的能力,其中一些能把非同步任務丟到背景;發起呼叫仍會上 call stack,但只是交接,真正的工作在背景跑、不擋堆疊;callback 型 API 完成後把 callback 排進 task queue;promise 相關的則進 microtask queue,它有優先權,而且每處理完一個 task 就再檢查一次。這五句話的順序就是第 2 段到第 8 段的順序——她刻意讓 recap 的結構和推導的結構一致。
AI 補充值得注意的是這段 recap 沒有新增任何規則,但它換了一個組織方式:前面是按「問題 → 解法」推導的,這裡是按「零件 → 職責」列的。這兩種視角都要有——推導的版本讓你知道為什麼需要這些機制,零件的版本讓你在除錯時能快速定位。實務上可以把它濃縮成一個判斷流程:先問這段程式碼是同步還是非同步(同步就現在跑完);非同步的話問它是誰完成的(web API 在背景做);完成後問它進哪個佇列(callback 型進 task queue、promise 相關進 microtask queue);最後問 call stack 空了沒、microtask queue 清空了沒。用這四問就能解釋幾乎所有「為什麼這行比那行晚印」的疑惑,也正是作者說「知道規則就能解釋程式為何在那個時間點執行」的意思。
12. 自己動手試才會真的懂 11:58–12:34
作者最後鼓勵:非同步 JavaScript 常常讓人挫折,但只要知道每個佇列的規則,就能解釋程式為什麼在那個時間點執行。建議自己拿 setTimeout 和 queueMicrotask 玩玩看,親手驗證順序。
推理因為前面所有內容都是靠「預測順序 → 驗證順序」建立起來的,所以作者最後給的建議也一致:自己拿 setTimeout 和 queueMicrotask 排列組合玩玩看,先猜再跑,看結果對不對。她把這件事講得很實際——非同步 JavaScript 常常讓人挫折,但那份挫折來自「不知道規則」,而不是「這件事很難」;規則就那幾條,知道之後每個執行時機都解釋得通。
AI 補充把這個建議變成可操作的練習,最有效的順序是照著推導的難度爬:先寫一支只有 console.log 和一個 setTimeout(fn, 0) 的腳本,確認同步永遠贏;再加一個 Promise.resolve().then,確認 microtask 贏過 task,且跟延遲寫多少無關;再把 .then 換成巢狀的 queueMicrotask,重現第 10 段那個「4 插在 2 前面」的結果;最後刻意寫一個遞迴的 queueMicrotask,親眼看到頁面卡死,就會記住那條優先權規則的代價。每次都先在紙上寫下預測再執行,錯的那一題才是你模型缺角的地方。瀏覽器開發者工具的 Performance 面板可以把這些 task 與 microtask 畫成時間軸,是驗證的好幫手。要留意的是 Node.js 多了 process.nextTick 與好幾個 phase,順序規則比瀏覽器複雜,練習時建議先固定在瀏覽器環境,模型穩了再看 Node.js 的差異。
4. 總結
整支影片是一條單線推導。作者先把 event loop 放回 runtime 全景裡,指出真正執行你的程式碼的只有 call stack;接著示範單一 call stack 如何用推入與彈出決定執行順序。這個模型有個沒說出口的前提:每個 execution context 都會很快彈出去。作者故意放一個重運算進去,整個程式凍結,於是問題成形:遇到網路請求、計時器這類必然很慢的工作怎麼辦。答案是 web APIs——瀏覽器提供的介面,其中一部分能把長任務卸載到背景,呼叫它只是登記與交接,登記完立刻彈出堆疊。但資料回來之後,callback 不能直接插回 call stack,否則會打斷正在跑的任務,於是需要一個緩衝區:task queue,以及一個負責搬運的角色:event loop,規則是 call stack 空了才搬。setTimeout 讓這條規則產生一個常見誤解——你指定的延遲,是到進入 task queue 為止的延遲,不是到執行為止。promise 型 API 則走另一條線:microtask queue,它有優先權,event loop 會先把它完全清空才碰 task queue,而且處理完每個 task 後再檢查一次;代價是 microtask 可以無限自我排隊,把程式餓死。最後那道測驗答案 51342,每一位數字都只由前面推導出的規則決定:同步先跑、microtask 全清、才輪到 task。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 8. Microtask queue 有優先權 8:27 |
「I believe in node you can set like Mex tick depth or something like that which prevents this exact thing from happening」 | 作者自己也用「我記得好像」的語氣說的,但這個設定在現代 Node.js 並不存在。她指的應該是舊版的 process.maxTickDepth,它在 Node 0.12 就被移除了,而且當年管的是 process.nextTick 的遞迴深度,並不是 promise/queueMicrotask 的 microtask。現在的 Node.js 沒有任何內建開關可以擋住無限 microtask 迴圈,行為和瀏覽器一樣會卡死,只能靠自己不要寫出會自我複製的 microtask。 依據: process.maxTickDepth 於 Node.js 0.12(2015)移除;Node.js 目前文件中無對應設定 |
| 8. Microtask queue 有優先權 6:37 |
「so only those callbacks or those function body parts get pushed onto the microtask CU so it's very specific」 | 「只有這些」是為了教學而收緊的說法。她列的四項(then/catch/finally、await 之後、queueMicrotask、MutationObserver)涵蓋了日常九成情況,但規格上還有其他來源也會排入 microtask,例如 custom element 的 reaction callbacks、動態 import() 的完成、Atomics.waitAsync。把它記成「promise 相關與少數瀏覽器內部機制」比記成一份封閉清單安全。 依據: HTML Standard「queue a microtask」與 WHATWG custom element reactions |
5. 推薦三個下一步
1. 往下挖深:event loop 在 Node.js 裡的樣子
影片講的是瀏覽器的模型,只順口提了一句 Node 的行為(而且那句已經過時)。Node 的 event loop 分成 timers、poll、check 等階段,還多了 process.nextTick 與 setImmediate,同一套規則在那裡有不同的細節。
YouTube 搜尋:Node.js event loop phases process.nextTick vs setImmediate libuv event loop explained
2. 往旁邊對照:渲染與 event loop 的關係
影片只講程式碼何時執行,沒講畫面何時更新。requestAnimationFrame 與瀏覽器的 rendering step 卡在 task 與 microtask 之間,補上這一塊才知道為什麼有些程式碼會造成掉幀。
YouTube 搜尋:requestAnimationFrame event loop browser rendering pipeline event loop Jake Archibald in the loop
3. 往上應用:用這套模型解真實的效能問題
知道規則之後,下一步是拿它去讀 Performance 面板上的 long task 與 INP,判斷該把工作切碎、丟進 web worker,還是改用 scheduler API。
YouTube 搜尋:long tasks main thread blocking web workers javascript performance scheduler.yield INP optimization
- 接著看 ←9 JavaScript Concepts That Got Me To Senior Dev · 九個概念用一節帶過 event loop,這站整支把兩個佇列與搬運規則拆開
- 接著看 ←JavaScript Visualized - Promise Execution · 作者明說先看 promise 那支;懂了 reaction record 才接得住 microtask queue 的優先權
- 接著看 →Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer) · 面試題直接考 event loop 的執行順序,先在這站把規則推熟
- 相關 —JavaScript Visualized - Closures · 同一系列的視覺化,都從 call stack 與 execution context 出發,先看誰都行
- 接著看 ←JavaScript Visualized - Execution Contexts · 這站走完 call stack 就結束,那站補上 web API 與兩個佇列讓順序不再連續
- 接著看 →Keep Those WebSocket Connections Alive! · 先懂 setTimeout 與單執行緒排程,才看得出 interval 與 pong 回呼為何不會 race