Promise 的執行模型:物件內部欄位、promise reaction record 與 microtask queue

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 15m16s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(8)

1. Outline

  1. 起點 · JavaScript Visualized - Promise Execution
    從記憶體裡的 promise 物件開始,一路推到 then 鏈與輸出順序,把 Promise 從「API 記憶」變成「可推導的執行模型」。

2. YouTuber 的思維推導

JavaScript Visualized - Promise Execution

Lydia Hallie · 8m42s · 字幕 en · vision=on (使用者指定 vision=true;這支影片整支是動畫示意圖(內部欄位、call stack、microtask queue 的搬移),作者全程指著畫面說「這裡」「這個 handler」,不看圖等於少一半資訊。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 4s
segment 47s 5m00s
shot 24s 4s
analyze 3m45s 9m16s
render 5s –

作者的出發點是一個心理問題:大家覺得 Promise 嚇人,是因為只看到 API(new Promise、then、catch),看不到引擎在底下做了什麼。所以她刻意不從「怎麼用」講起,而是從「記憶體裡有什麼」開始:先把 promise 物件的五個 internal slot 攤開,讓 resolve/reject 退化成「呼叫函式改物件屬性」這件毫無魔法的事,並收在一句「nothing special here」。接著她指出真正的機關就藏在剛剛刻意跳過的兩個欄位——fulfill/reject reactions,因為 then 會在那裡放一筆 promise reaction record,而 resolve 的最後一步是把帶著 result 的 handler 排進 microtask queue,非同步就從這一刻才開始。

為了讓「排進 queue」這句話有意義,她插入一段 event loop 複習(microtask queue 優先於 task queue),再把玩具範例校正回真實用法(executor 裡啟動一件離開主執行緒的工作),然後用 setTimeout + then 一格一格走完整條時間線,導出「等待期間 script 仍可繼續跑」的非阻塞結論。之後加一層:then 自己也回傳 promise,所以鏈式 then 能讓結果逐步累積加工。最後用一道「印出 1、3、2」的題目把所有零件收回來——resolve 是同步的、handler 是被排程而不是被執行、要等 call stack 空了才輪到 microtask——答得出來就代表整條執行模型真的看懂了。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. Promise 不可怕,只是看不見底層 → 既然要看「引擎到底做了什麼」,第一個必須回答的問題就是:new Promise 執行的那一瞬間,記憶體裡到底生出了什麼東西?
  2. new Promise 建了什麼物件 → 五個欄位裡有兩個(fulfill reactions、reject reactions)被作者刻意跳過,而且他還特別說「到這裡沒什麼特別的」——那 promise 特別的地方必然就在那兩個空欄位裡,下一段要問的是:誰會把東西放進去?
  3. then 建立 promise reaction record → handler 已經被放進 microtask queue 了,但「放進 queue」到底什麼時候會被拿出來執行?要判斷輸出順序,得先講清楚 queue 之間的優先權。
  4. 複習:microtask queue 優先於 task queue → 執行順序的判準有了,但目前所有範例的 resolve 都是同步寫在 constructor 裡的假動作——下一段得先把場景校正回真實用法:executor 裡到底該放什麼?
  5. executor 裡真正該放的東西 → 場景校正完了,接下來就把「constructor 裡放一個 setTimeout、後面接一個 then」這段最接近真實的程式碼,一格一格走完整條時間線。
  6. setTimeout + then 的逐格執行 → 這條時間線只處理了一個 then、一次結果。如果我想把結果再加工一次呢?線索是作者到目前為止刻意沒提的一件事:then 本身也有回傳值。
  7. then 也回傳 promise:鏈式處理 → 零件到這裡全部到位了:state/result、reaction record、microtask 優先權、鏈式。剩下的問題是自己算不算得出來——作者接著出一道輸出順序的題目來驗收。
  8. 小測驗:為什麼印出 1、3、2

3. 逐段說明

JavaScript Visualized - Promise Execution

1. Promise 不可怕,只是看不見底層 0:00–0:19

作者開場說 Promise 常讓人覺得嚇人、煩躁,但那是因為看不到它在底下做了什麼。這支影片要做的事就是把「跟 Promise 互動時,引擎到底發生什麼」畫出來。定調:理解 Promise 不是背 API,而是看懂執行過程。

承上 前情提要裡「你已經會寫 then/catch,卻說不出它為什麼晚一步執行」這個背景,被作者直接拿來當開場:她假設觀眾缺的不是語法,而是執行過程的畫面。

推理因為觀眾對 Promise 的抗拒來自「看不見底下發生什麼」,所以作者不從語法教起,而是先把問題重新定義成一個可視化問題——只要把引擎在跟 promise 互動時做的事畫出來,那種複雜感就會消失。她用「I promise you」開玩笑,同時把整支影片的題目定成 promise execution(執行過程),不是 promise usage(用法)。

AI 補充讓人覺得 daunting 的通常不是 then/catch 的寫法,而是三件看不見的事:promise 物件自己的狀態、我們傳進去的 callback 被存到哪裡、以及它到底什麼時候被拿出來跑。這支影片的路線正好照這三件事的順序走,所以接下來看到的第一件東西會是「記憶體裡的物件長什麼樣」,而不是「怎麼寫 async/await」。先建立這個期待,後面每一格動畫才知道要看哪裡。

Promise

承諾/期約

一個代表「尚未完成、但未來會有結果(成功值或錯誤)」的 JavaScript 物件。

Promise 常被誤解成「讓程式變快」或「開了另一條執行緒」的工具,它兩者都不是。它只是一個狀態機物件:現在是 pending,未來會變成 fulfilled(帶一個值)或 rejected(帶一個錯誤),而且只會變一次。真正在等待的工作是執行環境(瀏覽器或 Node)在做,Promise 只負責記住結果、以及記住有誰想在結果出來時被通知。因為「被通知」這件事要透過 queue 排程,所以 then 的 callback 一定不是同步執行——這是整支影片要證明的那件事。

相關術語: executor function (由它啟動)、microtask queue (通知靠它排程)

出處:第 1 段「Promise 不可怕,只是看不見底層」

留給下一段 既然要看「引擎到底做了什麼」,第一個必須回答的問題就是:new Promise 執行的那一瞬間,記憶體裡到底生出了什麼東西?

2. new Promise 建了什麼物件 0:19–1:15

用 new Promise 搭配 executor function 建立 promise 時,記憶體裡會生出一個 promise 物件,裡面有幾個 internal slot:promise state、promise result、promise fulfill reactions、promise reject reactions、promise is handled。呼叫 resolve 會把 state 設成 fulfilled、result 設成傳入的值;呼叫 reject 則設成 rejected 與對應的值。作者強調到這裡完全不特別——只是呼叫函式改物件屬性而已。

promise 物件的五個 internal slot 全部列出來的那一格,是後面所有推理的底圖
0:40 · promise 物件的五個 internal slot 全部列出來的那一格,是後面所有推理的底圖
呼叫 resolve 後 state 變 fulfilled、result 變 "done" 的前後對照,看得到欄位被寫入
0:58 · 呼叫 resolve 後 state 變 fulfilled、result 變 "done" 的前後對照,看得到欄位被寫入
承上 上一段留下的問題是「new Promise 執行時記憶體裡生出什麼」,這一段就把那個物件整個攤開來,一格一格點名它的欄位。

推理因為上一段把 Promise 重新定義成「看得見執行過程就不難」,所以作者接著做最基礎的一步:呼叫 new Promise 時記憶體裡建出一個 promise 物件,列出它的 internal slot(promise state、promise result、fulfill reactions、reject reactions、is handled),再示範呼叫 resolve 只是把 state 寫成 fulfilled、result 寫成 "Done!",呼叫 reject 就寫成 rejected 與 "Fail!"。她刻意把這一段收在「nothing special here,我們只是呼叫函式去改物件屬性」,好讓下一段的反差成立。

AI 補充三點值得補:(1)作者說 resolve 是「executor function 提供給我們的」,方向其實相反——resolve 與 reject 是 Promise constructor 造出來的兩個函式,被當成參數傳進 executor,executor 只是收到它們;方向搞反會誤以為能在別的地方拿到某顆 promise 的 resolve。(2)畫面右側還有一塊她沒念出名字的方框:Promise Capability,裡面裝 [[Promise]]、[[Resolve]]、[[Reject]],規格用它把「一顆 promise + 專門操作它的兩個函式」綁成一組,這正是為什麼 resolve 只能改到自己那顆 promise。(3)狀態只能單向轉一次:pending → fulfilled 或 pending → rejected,之後再呼叫 resolve/reject 完全沒有效果,不報錯也不會改值——截圖裡 [[PromiseState]] 從 "pending"、[[PromiseResult]] 從 undefined 起跑,就是這條單行道的起點。另外要注意 executor 是同步跑的:new Promise 還沒回傳,executor 裡的程式碼就已經執行完了。

術語:Promise Capability Record

Promise constructor

Promise 建構子

用 new Promise(...) 呼叫、負責在記憶體裡建出一顆新 promise 物件的函式。

它做三件事:建出 promise 物件並把 state 設為 pending、造出這顆 promise 專屬的 resolve 與 reject 兩個函式、然後立刻同步呼叫你傳進去的 executor 並把那兩個函式交給它。因為是同步呼叫,executor 裡的第一行程式碼會比 new Promise 後面那一行更早跑。實務上只有在「要把一個 callback 式 API 包成 promise」時才需要自己寫 new Promise;一般情況直接用已經回傳 promise 的 API(fetch 之類)就好。

相關術語: executor function (同步呼叫它)、Promise Capability Record (產出這組)

出處:第 2 段「new Promise 建了什麼物件」

executor function

執行器函式

傳給 new Promise 的那個函式,簽名是 (resolve, reject) => {...},用來啟動工作並決定結果。

它最容易被誤會的一點是「同步」:executor 的內容在 new Promise 回傳之前就跑完了,只有它裡面啟動的非同步工作會晚一點回來。它拿到的 resolve/reject 是外面世界唯一能操作這顆 promise 的把手,所以常見的把手外流寫法(把 resolve 存到外部變數)雖然能跑,但會讓誰結束這顆 promise 變得難以追蹤。executor 裡丟出的同步錯誤會被自動轉成 reject。

相關術語: resolve (收到它)、asynchronous task (在裡面啟動)

出處:第 2 段「new Promise 建了什麼物件」

internal slot

內部欄位

ECMAScript 規格層面的隱藏欄位,用 [[Name]] 表示,JavaScript 程式碼無法直接讀寫。

影片裡畫成方格的 [[PromiseState]]、[[PromiseResult]] 等都是 internal slot,不是普通屬性:你不能寫 p["[[PromiseState]]"] 把它讀出來,只能透過 then/catch 或開發者工具間接看到。規格用它來描述「引擎必須記住這些資訊」,各家引擎實作方式可以不同。理解它的價值在於:一旦知道狀態與結果是被記在物件裡的欄位,就會明白為什麼 promise settle 之後再串 then 依然拿得到值。

相關術語: [[PromiseState]] (一種)、ECMAScript specification (定義於)

出處:第 2 段「new Promise 建了什麼物件」

[[PromiseState]]

promise 狀態欄位

記錄這顆 promise 目前是 pending、fulfilled 還是 rejected 的內部欄位。

初始值是 "pending",被 resolve 改成 "fulfilled"、被 reject 改成 "rejected",而且只能改一次,之後就凍結——這叫 settled。截圖裡它從 "pending" 變成 "fulfilled" 的那一格,就是整顆 promise 一生中唯一的狀態轉換。狀態決定的是「之後串上的 handler 該進 fulfill 還是 reject 那一列」,而不是「什麼時候執行」。

相關術語: [[PromiseResult]] (同時被寫入)、resolve (被它改寫)

出處:第 2 段「new Promise 建了什麼物件」

[[PromiseResult]]

promise 結果欄位

存放 resolve 或 reject 傳進來的那個值(成功值或錯誤)的內部欄位。

它初始是 undefined,settle 時被寫入一次。這個欄位就是後來 then 的 handler 收到的那個參數的來源:handler 不是「等著誰傳值給它」,而是引擎從這個欄位把值抓出來塞給它。因為值被存住了,所以在 promise 已經 fulfilled 之後才串 then,一樣讀得到同一個值。

相關術語: handler (值餵給它)、[[PromiseState]] (成對)

出處:第 2 段「new Promise 建了什麼物件」

resolve

完成(把 promise 設為成功)

executor 收到的函式之一,呼叫它會把 state 設成 fulfilled、result 設成傳入的值。

它是一個普通函式,可以傳來傳去、可以在 callback 裡呼叫,這也是為什麼 setTimeout 的 callback 能結束一顆 promise。第二次呼叫沒有任何作用,因為狀態已經凍結。另外它有一個影片沒提的特別行為:如果傳進去的值本身是一個 promise 或 thenable,這顆 promise 不會立刻 fulfilled,而是改為跟隨那顆 promise 的結果。

相關術語: reject (相反)、[[PromiseState]] (改寫它)

出處:第 2 段「new Promise 建了什麼物件」

reject

拒絕(把 promise 設為失敗)

executor 收到的另一個函式,呼叫它會把 state 設成 rejected、result 設成傳入的錯誤值。

習慣上傳一個 Error 物件而不是字串,這樣才有 stack trace 可看。它和 resolve 共用同一條單行道:先呼叫到的那個贏,另一個之後就無效。若一顆 promise 被 rejected 而始終沒有人用 catch 接手,執行環境會發出 unhandled rejection 警告,這件事跟第五個欄位 [[PromiseIsHandled]] 直接相關。

相關術語: resolve (相反)、catch (由它接手)

出處:第 2 段「new Promise 建了什麼物件」

Promise Capability Record

promise 能力記錄

規格用來把一顆 promise 與它專屬的 resolve/reject 兩個函式綁成一組的內部結構。

截圖右側那塊 Promise Capability 就是它,欄位是 [[Promise]]、[[Resolve]]、[[Reject]],作者畫了但沒念名字。有了這個綁定,引擎才知道某個 resolve 函式該去改哪一顆 promise 的欄位。它也解釋了後面鏈式的機制:每個 then 回傳的新 promise 都會配一組自己的 capability,handler 跑完之後就是用那組把下游 promise 結束掉。

相關術語: Promise constructor (由它建立)、Promise Reaction Record (對照組)

出處:第 2 段「new Promise 建了什麼物件」

留給下一段 五個欄位裡有兩個(fulfill reactions、reject reactions)被作者刻意跳過,而且他還特別說「到這裡沒什麼特別的」——那 promise 特別的地方必然就在那兩個空欄位裡,下一段要問的是:誰會把東西放進去?

3. then 建立 promise reaction record 1:15–2:10

Promise 真正特別的地方在剛剛跳過的兩個欄位:fulfill reactions 與 reject reactions,它們裝的是 promise reaction record。串上 then 或 catch 就會建立一筆 record,record 裡的 handler 帶著我們傳給 then 的 callback。之後呼叫 resolve 時:resolve 進 call stack、state 變 fulfilled、result 被寫入,handler 收到這個 result,然後 handler 被排進 microtask queue——非同步的部分從這裡才開始。

then 建立的 promise reaction record 與它的 handler/code 欄位,看得出 callback 被存在哪裡
1:44 · then 建立的 promise reaction record 與它的 handler/code 欄位,看得出 callback 被存在哪裡
handler 帶著 result 被送進 microtask queue 的瞬間,是「同步變非同步」的交界點
2:08 · handler 帶著 result 被送進 microtask queue 的瞬間,是「同步變非同步」的交界點
承上 上一段留下的線索是「特別之處藏在被跳過的 fulfill/reject reactions 兩個空欄位」,這一段就回答誰把東西放進去:串上 then 或 catch 的那一刻。

推理因為上一段刻意證明了 resolve 只是改屬性、沒什麼特別,所以作者接著把落差補上:then 的職責是建立一筆 promise reaction record 放進 fulfill reactions,record 裡的 handler 包著我們傳給 then 的那段 callback。於是 resolve 的動作多出了一步——寫完 state 與 result 之後,把拿到 result 的 handler 排進 microtask queue。她在這裡明確宣告:非同步是從這一步才開始的。

AI 補充關鍵是把「存起來」和「執行」分成兩件事看:then 只負責存,觸發由 resolve 負責,實際執行時機由 event loop 決定——這就是 then 的 callback 為什麼永遠不會同步跑。對照兩張截圖還能抓到一個作者列了名字但完全沒解釋的欄位:只有 promise 物件、還沒串 then 時 [[PromiseIsHandled]] 是 false,串上 then 之後變成 true。它的用途是讓執行環境知道「這顆 promise 有人接手了嗎」,一顆 rejected 而始終沒人接手的 promise 就是靠它被判定成 unhandled rejection。另外 reaction record 裡除了 handler,還記著「handler 跑完要去 resolve 哪一顆下游 promise」(畫面上被略過的那幾條灰色欄位),這是鏈式能成立的伏筆;而 fulfill reactions 是一個列表而不是單格,因為同一顆 promise 上可以串好幾個 then,每個都放一筆。

術語:[[PromiseFulfillReactions]][[PromiseRejectReactions]]

Promise Reaction Record

promise 反應記錄

then 或 catch 建立的一筆內部記錄,記著「結果出來時要跑哪段程式、跑完要結束哪顆 promise」。

它是整支影片的核心零件:promise 物件本身只存狀態與值,「有誰在等」是存在這些 record 裡。record 至少包含 handler(你的 callback)、type(fulfill 還是 reject)以及一組指向下游 promise 的 capability。promise 還是 pending 時,record 被存進 fulfill/reject reactions 列表等候;promise 已經 settle 時,則不必存進列表,直接排一份工作去跑。把它想成「訂閱單」最貼切:then 的動作是下訂,不是取貨。

相關術語: handler (包含它)、[[PromiseFulfillReactions]] (存放於)

出處:第 3 段「then 建立 promise reaction record」

[[PromiseFulfillReactions]]

成功反應列表

存放「這顆 promise 成功時要執行的 reaction record」的內部欄位。

上一段被刻意跳過的兩個空格之一。它是列表,所以同一顆 promise 串三個 then 就會有三筆 record,並且在 resolve 時依加入順序全部排進 microtask queue。promise settle 之後這個列表會被清空,因為記錄已經沒有用途了。

相關術語: [[PromiseRejectReactions]] (對照組)、Promise Reaction Record (存放它)

出處:第 3 段「then 建立 promise reaction record」

[[PromiseRejectReactions]]

失敗反應列表

存放「這顆 promise 失敗時要執行的 reaction record」的內部欄位。

與成功列表完全對稱,只是被 reject 觸發。寫 then(onOk, onErr) 會同時往兩個列表各放一筆;寫 catch(fn) 等於只往這個列表放一筆。理解這一點就會知道 then 與 catch 沒有本質差別,catch 只是 then(undefined, fn) 的語法糖。

相關術語: [[PromiseFulfillReactions]] (對照組)、reject (被它觸發)

出處:第 3 段「then 建立 promise reaction record」

then

串接成功處理

掛在 promise 上的方法,負責建立一筆 reaction record,並回傳一顆新的 promise。

它的名字讓人以為是「然後執行」,實際上呼叫 then 的當下什麼都不會執行,只是登記。它一定馬上回傳一顆新的 pending promise,這顆新 promise 的結果由 handler 的回傳值決定。要注意 then 是登記行為,所以在同一顆 promise 上呼叫兩次 then 是「兩個訂閱者」,不是「兩個步驟」——那和串成一條鏈的意義完全不同。

相關術語: catch (同一機制)、Promise Reaction Record (建立它)

出處:第 3 段「then 建立 promise reaction record」

catch

串接失敗處理

掛在 promise 上的方法,等同 then(undefined, onRejected),用來接手 rejected 的結果。

它同樣是登記而非執行,同樣回傳一顆新的 promise,所以 catch 後面還能接 then。因為 reject reactions 用的是同一套 record 機制,錯誤會沿著鏈往下傳到第一個 catch,這就是為什麼把 catch 放在鏈尾就能接住整條鏈的錯誤。反過來說,catch 放在鏈的中間,它後面的 then 仍會照跑,因為 catch 回傳的那顆 promise 是 fulfilled 的。

相關術語: then (同一機制)、[[PromiseRejectReactions]] (登記到)

出處:第 3 段「then 建立 promise reaction record」

handler

處理函式

reaction record 裡實際包著我們傳給 then/catch 的那段 callback 的欄位。

截圖裡 [[Handler]] 方格裡直接顯示 result => console.log(result),也就是我們的原始碼被搬到了物件裡面。它是被排進 queue 的最小單位:queue 裡排的不是 promise、不是 then,就是 handler。當 handler 被取出執行時,[[PromiseResult]] 的值會被當成參數傳進去——截圖裡 "Done!" 直接跳進參數位置的那一格畫的就是這件事。

相關術語: callback (包裝它)、microtask queue (被排進)

出處:第 3 段「then 建立 promise reaction record」

callback

回呼函式

交給別人、等某件事發生時再被呼叫的函式。

Promise 沒有取代 callback,只是換了呼叫它的人:以前是 API 直接呼叫你的 callback,現在是引擎從 queue 取出 handler 來呼叫。這個差別帶來兩個好處:呼叫時機統一(永遠在 call stack 清空後)、以及錯誤可以沿鏈傳遞。也因為多繞了 queue 這一圈,promise 的 callback 一定比同步程式碼晚跑。

相關術語: handler (被包成)、asynchronous task (常見搭配)

出處:第 3 段「then 建立 promise reaction record」

microtask queue

微任務佇列

專門排放 promise handler 這類「要盡快、但不插隊」的工作的佇列。

promise 的非同步性完全來自這個 queue:resolve 的最後一步就是把 handler 排進來。它的特點是優先權高但不搶佔——不會打斷正在執行的程式碼,卻會在 call stack 清空後立刻被清乾淨,比任何 setTimeout 都早。除了 promise handler,queueMicrotask 與 MutationObserver 的 callback 也排在這裡。

相關術語: handler (排放它)、task queue (優先於)

出處:第 3 段「then 建立 promise reaction record」

[[PromiseIsHandled]]

是否已被接手

記錄這顆 promise 有沒有人用 then/catch 接手的內部布林欄位。

作者念了名字但沒解釋它;比對兩張截圖可以看到它從 false 變成 true,時機正是串上 then。它的用途是判定 unhandled rejection:一顆 rejected 且這個欄位仍是 false 的 promise,瀏覽器會觸發 unhandledrejection 事件,Node.js 預設會印警告甚至結束行程。這也是為什麼「先 reject、很久以後才串 catch」有時仍會看到警告——判定發生在你接手之前。

相關術語: reject (警告來源)、then (被它設為 true)

出處:第 3 段「then 建立 promise reaction record」

留給下一段 handler 已經被放進 microtask queue 了,但「放進 queue」到底什麼時候會被拿出來執行?要判斷輸出順序,得先講清楚 queue 之間的優先權。

4. 複習:microtask queue 優先於 task queue 2:10–2:30

作者插入一段 event loop 快速複習:call stack 一空,event loop 先看 microtask queue,等 microtask queue 清空才輪到 task queue(callback queue、macrotask queue)。重點只有一句——microtask queue 有優先權。

call stack、microtask queue、task queue 三者關係的全景圖,決定後面兩個範例的輸出順序
2:25 · call stack、microtask queue、task queue 三者關係的全景圖,決定後面兩個範例的輸出順序
承上 上一段停在「handler 被排進 microtask queue」,這一段就補上那個 queue 什麼時候會被清空——不然「排進去」只是一句沒有時間感的話。

推理因為上一段把非同步的起點交給了 microtask queue,所以作者插入一段 event loop 快速複習:call stack 一空,event loop 先看 microtask queue,等它清空才輪到 task queue(也有人叫 callback queue 或 macrotask queue)。她只要求記住一句「microtask queue 有優先權」,因為後面兩個範例的輸出順序全靠這一句判定。

AI 補充截圖裡四個框的分工值得記牢:call stack 是現在正在跑的、Web APIs 是交給執行環境去等的、task queue 放 setTimeout 這類 callback、microtask queue 放 promise handler。更精確的模型是「每跑完一個 task 就把 microtask queue 整個清空」,而不是「microtask 全部清完才開始跑 task」;而且清空過程中新產生的 microtask 會接在同一輪繼續跑,所以不斷產生 microtask 會把畫面更新餓死。環境差異也要知道:瀏覽器的渲染(樣式計算、layout、paint)排在清空 microtask 之後,而 Node.js 還多一條 process.nextTick 佇列,比 promise 的 microtask 更優先。

event loop

事件循環

不斷檢查「call stack 空了嗎、哪個 queue 有東西」並把工作搬上 call stack 的機制。

它不是 JavaScript 語言的一部分,而是執行環境(瀏覽器、Node.js)提供的。它只做搬運,不做執行:東西被搬上 call stack 之後就跑到底,中間不會被打斷,這叫 run-to-completion。理解這一點就知道為什麼一段同步的重運算會凍住整個頁面——event loop 根本沒有機會搬下一件事。

相關術語: call stack (搬上它)、microtask queue (優先檢查)

出處:第 4 段「複習:microtask queue 優先於 task queue」

call stack

呼叫堆疊

記錄目前正在執行的函式呼叫的堆疊;空了才代表 JavaScript 這一刻沒事做。

影片裡每一格動畫的重點都在看它有沒有空:new Promise、setTimeout、resolve、handler 都要先被推上 call stack 才會執行,執行完就彈出。「call stack 空了」是 event loop 唯一的動作時機,也是判斷輸出順序的錨點。它同時解釋了為什麼遞迴太深會 stack overflow。

相關術語: event loop (被它填入)、synchronous (在其中執行)

出處:第 4 段「複習:microtask queue 優先於 task queue」

microtask

微任務

被排進 microtask queue 的單一工作,例如一個 promise handler。

常說的「一輪 microtask」或「一個 tick」指的就是它被取出執行一次。鏈式 then 的每一環都各花一個 microtask,所以鏈越長,最終結果就越晚出現,但仍然遠早於任何 setTimeout(0)。要手動排一個 microtask 可以用 queueMicrotask()。

相關術語: microtask queue (排放於)、task queue (對照組)

出處:第 4 段「複習:microtask queue 優先於 task queue」

task queue

任務佇列(巨任務佇列)

存放 setTimeout、事件 callback 等工作的佇列,優先權低於 microtask queue。

作者提到它還有 callback queue、macrotask queue 等別名,名字不重要,重要的是排隊順序。實際上瀏覽器有多條 task queue(計時器、使用者輸入、網路等不同 task source),而且可以自行決定下一輪先取哪一條,所以兩個不同來源的 task 之間並沒有保證順序。這也是為什麼「setTimeout 0 一定比 promise 晚」成立,但「兩個 setTimeout 之間的相對順序」在跨來源時就不一定。

相關術語: microtask queue (優先權低於)、setTimeout (來源之一)

出處:第 4 段「複習:microtask queue 優先於 task queue」

見仁見智 2:14
「stack is empty the event Loop first checks in the microtask que and whenever this queue is empty it goes to the task que」
這個順序描述夠用來判斷輸出,但不是規格的實際流程。HTML 規格的 event loop 是「取一個 task 執行 → 立刻做一次 microtask checkpoint 把 microtask queue 清空 → 可能渲染 → 再取下一個 task」,也就是每個 task 後面都跟一次完整的 microtask 清空,而不是「microtask queue 空了才開始跑 task queue」。另外瀏覽器有多條 task queue(不同 task source)可自行挑選,Node.js 還多一條優先權更高的 process.nextTick 佇列。
依據: HTML Standard, 8.1.7.3 Event loop processing model(microtask checkpoint);Node.js docs, The Node.js Event Loop
留給下一段 執行順序的判準有了,但目前所有範例的 resolve 都是同步寫在 constructor 裡的假動作——下一段得先把場景校正回真實用法:executor 裡到底該放什麼?

5. executor 裡真正該放的東西 2:30–3:02

前面為了簡化,resolve 和 reject 都是在 constructor 裡同步呼叫的。實務上會在 executor 裡啟動非同步工作——任何離開主執行緒的事情:讀檔案、網路請求、甚至只是一個 timer。等它們回傳資料,就在它們的 callback 裡 resolve 資料、或在出錯時 reject。

承上 上一段給了輸出順序的判準,也順帶暴露了前面範例的不真實:resolve 一直是同步寫在 constructor 裡的。這一段就把場景校正回實務。

推理因為前面為了畫圖方便都同步呼叫 resolve,所以作者在進入完整推演之前先把 executor 的正確用法講清楚:在裡面啟動一件離開主執行緒的工作——讀檔案系統、發網路請求、或單純只是一個 timer——等它的 callback 帶著資料回來,再用資料 resolve,或在出錯時 reject。這樣後面那條時間線才有真實意義。

AI 補充「離開主執行緒」的實際意思是:那件事不是由 JavaScript 引擎自己算,而是交給執行環境(瀏覽器或 Node)去做,所以 JS 這條執行緒可以繼續往下跑。這同時界定了 Promise 不做什麼——它不會讓程式變成多執行緒,也不會讓同步的重運算變快;它只是把「一件正在等的工作何時完成」包成一個可以掛 handler 的物件。所以 new Promise 幾乎總是出現在「把舊的 callback 式 API 包起來」的場合(fs.readFile、XMLHttpRequest、setTimeout),這個動作一般叫 promisify。反過來說,如果 executor 裡完全沒有非同步工作,那顆 promise 只是繞了一圈的同步值,除了強迫結果延後一輪 microtask 之外沒有任何好處。

main thread

主執行緒

執行 JavaScript 的那一條執行緒;同一時間只能跑一件事。

瀏覽器裡它同時也負責處理事件與畫面更新,所以只要有一段同步程式碼跑太久,頁面就會卡住不能點。所謂「非同步」的本質,就是把等待交給主執行緒以外的地方(作業系統的計時器、網路堆疊、worker),主執行緒只在結果回來時處理一下。這是理解非阻塞的前提:省下來的不是計算,是等待。

相關術語: asynchronous task (離開它)、call stack (屬於它)

出處:第 5 段「executor 裡真正該放的東西」

asynchronous task

非同步工作

作者的定義:任何離開主執行緒的事情,例如讀檔、網路請求、計時器。

判斷一件事是不是非同步,看的不是有沒有用 promise,而是它會不會把等待交給執行環境。for 迴圈算一億次是同步的重工作,即使包在 promise 裡也一樣卡住頁面;fetch 一個網址即使寫成 callback 風格也仍然是非同步的。這個定義也說明了 promise 與非同步的關係:promise 是結果的容器,非同步是工作的性質,兩者可以分開存在。

相關術語: main thread (離開它)、non-blocking (前提)

出處:第 5 段「executor 裡真正該放的東西」

promisify

包成 promise

把一個 callback 式的非同步 API 包進 new Promise,讓它改為回傳 promise 的做法。

這就是作者在 executor 裡啟動工作、再於 callback 中 resolve/reject 的那個模式的正式名稱。Node.js 內建 util.promisify 可以自動做這件事,前提是那個 API 遵守 (err, data) => {} 的 callback 慣例。要注意包裝時必須確保每條路徑都會呼叫 resolve 或 reject,否則那顆 promise 會永遠 pending——這是實務上最常見的 promise bug。

相關術語: callback (包裝它)、executor function (實作於)

出處:第 5 段「executor 裡真正該放的東西」

留給下一段 場景校正完了,接下來就把「constructor 裡放一個 setTimeout、後面接一個 then」這段最接近真實的程式碼,一格一格走完整條時間線。

6. setTimeout + then 的逐格執行 3:02–4:43

以「constructor 裡有 setTimeout、後面接一個 then」的程式碼逐步走一遍:constructor 進 call stack 建出 promise 物件;executor 執行、setTimeout 進 call stack 排定 100 毫秒的計時器;then 進 call stack 建出 reaction record 後彈出。計時器到期,callback 進 task queue;script 跑完、call stack 空了,callback 才進 call stack 呼叫 resolve,改 state 與 result,並把 handler 排進 microtask queue;call stack 再度清空後,handler 才被取出執行、印出 done。作者收尾強調:正因為它走 microtask queue,等待期間 script 可以繼續跑、保持互動,這就是非阻塞。

setTimeout 把 100 毫秒的計時器與 callback 排出去的那一格,看得到誰持有 resolve
3:26 · setTimeout 把 100 毫秒的計時器與 callback 排出去的那一格,看得到誰持有 resolve
計時器到期、callback 從 timer 掉進 task queue 的瞬間
3:52 · 計時器到期、callback 從 timer 掉進 task queue 的瞬間
resolve 執行後 handler 被排進 microtask queue,是理解輸出順序的關鍵畫面
4:07 · resolve 執行後 handler 被排進 microtask queue,是理解輸出順序的關鍵畫面
承上 上一段把 executor 裡該放的東西定成「一件離開主執行緒的工作」,這一段就挑最小的那一種——setTimeout——把整條時間線從頭走到尾。

推理因為前面三段已經分別建立了物件欄位、reaction record 與 queue 優先權,所以作者這時候才敢逐格推演:new Promise 進 call stack 建出物件 → executor 同步執行,setTimeout 進 call stack,把 100 毫秒的計時器與 callback 交給環境 → then 進 call stack 建出 reaction record 後彈出 → 計時器到期,callback 落進 task queue → script 跑完、call stack 空了,callback 才進 call stack 呼叫 resolve,改 state 與 result,並把 handler 排進 microtask queue → call stack 再度清空,handler 才被取出執行印出 done。她用這條時間線導出結論:等待期間 script 一直可以繼續跑、保持互動,這就是非阻塞。

AI 補充這段有兩次「等到 call stack 空」,很容易被誤讀成同一次:第一次是等 script 跑完,setTimeout 的 callback 才從 task queue 進來;第二次是等那個 callback(含 resolve)跑完,handler 才從 microtask queue 進來。少算一次就會把輸出順序算錯。截圖裡 Web APIs 那格放的是「Delay 100 + 我們給 setTimeout 的 callback」,那個 callback 才是持有 resolve 的人——promise 自己完全不知道有 timer 存在,是 callback 主動去 resolve 它,所以計時器被取消時那顆 promise 就永遠 pending。另外 100 毫秒是「最早何時可以進 task queue」而不是「何時執行」:真到那時 call stack 還忙著,它就得繼續排隊,所以 setTimeout 的延遲永遠只是下限。最後,「非阻塞」的功勞其實分兩層:等待本身是環境在等,不佔 JS 執行緒;結果的處理被排進 microtask queue,不會插隊打斷正在跑的程式。

術語:Web API

setTimeout

計時器

把一個 callback 交給執行環境,指定最少等幾毫秒後排進 task queue 的 API。

它常被當成「延遲執行」,準確的說法是「延遲排隊」:時間到只保證 callback 進入 task queue,真正執行還要等 call stack 清空、且前面沒有更早的 task。所以 setTimeout(fn, 0) 也不會立刻跑,而且一定晚於任何已排好的 promise handler。在影片裡它扮演的是「最簡單的非同步工作」,用來示範 resolve 可以由別人在未來呼叫。

相關術語: task queue (排進它)、Web API (提供者)

出處:第 6 段「setTimeout + then 的逐格執行」

Web API

瀏覽器提供的介面

由執行環境(而非 JS 引擎)提供、負責在背景處理等待工作的功能,例如計時器與 fetch。

截圖裡 Web APIs 那一格就是「工作正在環境手上」的意思:JS 已經把它交出去了,call stack 是空的。這一格是理解非同步最關鍵的一格——它證明等待完全不佔用 JS 執行緒。在 Node.js 裡對應的角色是 libuv 與 C++ 綁定,名字不同但位置一樣。

相關術語: main thread (在其外)、task queue (完成後排入)

出處:第 6 段「setTimeout + then 的逐格執行」

non-blocking

非阻塞

等待與結果處理都不會佔住主執行緒,程式在期間仍可繼續執行並保持互動。

作者這裡下的結論就是它。要成立需要兩件事同時發生:等待交給環境(不佔執行緒),以及結果處理走 queue(不插隊)。反例是 while 迴圈忙等或同步 XHR:即使邏輯上在「等」,主執行緒仍被佔住,畫面就會凍住。要注意非阻塞不等於平行——同一時間仍然只有一件 JS 在跑。

相關術語: asynchronous task (依賴它)、microtask queue (實作於)

出處:第 6 段「setTimeout + then 的逐格執行」

留給下一段 這條時間線只處理了一個 then、一次結果。如果我想把結果再加工一次呢?線索是作者到目前為止刻意沒提的一件事:then 本身也有回傳值。

7. then 也回傳 promise:鏈式處理 4:43–6:25

then 除了建立 reaction record,本身也會回傳一個新的 promise 物件,因此可以把多個 then 串起來、讓結果逐步累積。範例:promise 立刻 resolve 為 1,第一個 then 的 handler 回傳 result*2,於是它回傳的 promise 被設為 fulfilled、result 為 2;第二個 then 同樣的 handler 得到 2,算出 4;第三個 then 只 console.log,沒有 return,所以那顆 promise 的 result 是 undefined,但主控台會印出 4。作者說真實情況不會是數字,而像是把圖片先縮放、再加濾鏡、再換格式——用鏈式 then 非阻塞地一步步處理。

第一個 then 同時建立 reaction record 與新的 promise 物件,這張圖解釋鏈式為什麼成立
5:15 · 第一個 then 同時建立 reaction record 與新的 promise 物件,這張圖解釋鏈式為什麼成立
1 → 2 → 4 沿著鏈往下傳的對照畫面,看得出每一顆 promise 各自的 result
5:40 · 1 → 2 → 4 沿著鏈往下傳的對照畫面,看得出每一顆 promise 各自的 result
承上 上一段結尾留下的限制是「一個 then 只處理一次結果」,這一段就用作者刻意留到現在才講的那件事——then 的回傳值——把限制解掉。

推理因為上一段的推演停在單一 handler,所以作者補上 then 的第二個身分:它除了建立 reaction record,還會回傳一顆新的 promise 物件。於是 handler 的回傳值就成為那顆新 promise 的 result:promise 立刻用 1 resolve,第一個 then 的 handler 算出 result * 2 得到 2,第二個 then 拿到 2 算出 4,第三個 then 只有 console.log 沒有 return,所以它那顆 promise 的 result 是 undefined,但主控台仍印出 4。她再把數字換成「圖片先縮放、再加濾鏡、再換格式」,說明鏈式的真正用途是逐步加工。

AI 補充截圖裡兩顆 Promise Object 並排出現是最重要的線索:鏈上每一個 then 都有自己的一顆 promise、自己的 state 與 result,平常口語說的「同一顆 promise」其實是一串。動畫看起來像是 1 → 2 → 4 一瞬間算完,實際上每一環都要各花一輪 microtask:第一個 handler 執行完才 resolve 第二顆 promise,才排第二個 handler;三個 then 就是三輪,所以鏈越長,結果出現得越晚(雖然仍遠比阻塞便宜)。另外兩個作者沒提但很常踩的點:如果 handler 回傳的是一顆 promise 而不是數字,下游不會拿到「一顆 promise」,而會等它 settle 再取它的值,這個吸收行為讓 then 鏈可以串接非同步步驟;而 handler 裡丟出的錯誤會沿著鏈往下找第一個 catch,因為 reject reactions 用的是同一套 record 機制——這就是 catch 放在鏈尾能接住整條鏈的原因。忘記 return 是鏈式最常見的 bug:下游會拿到 undefined,正如影片裡第三個 then 那顆 promise 的下場。

術語:promise chainingthenable

promise chaining

promise 鏈式串接

利用 then 回傳新 promise 的特性,把多個處理步驟串成一條可逐步加工結果的鏈。

成立的條件是 then 一定回傳新 promise,而那顆 promise 的結果由 handler 的回傳值決定。鏈上每一環都是獨立的 promise,各有自己的 state 與 result,所以錯誤可以往下傳、結果可以逐段轉換。要分清楚「串鏈」與「同一顆 promise 上呼叫多次 then」:前者是 a.then(f).then(g),g 拿到 f 的結果;後者是 a.then(f); a.then(g),f 與 g 拿到同一個值、互不相干。

相關術語: then (依賴它)、thenable (延伸機制)

出處:第 7 段「then 也回傳 promise:鏈式處理」

thenable

具備 then 的物件

任何有 then 方法的物件;promise 會把它當成 promise 來等待並取其結果。

這解釋了 handler 回傳一顆 promise 時發生的事:下游不會收到那顆 promise 本身,而是等它 settle 再拿裡面的值,這個動作叫 resolution(解析)。也因此 promise 不會嵌套成兩層——resolve 一顆 promise 會自動攤平。代價是多花一到兩輪 microtask,這是「回傳 promise 的 then 比回傳純值的 then 慢半拍」的原因。

相關術語: resolve (被它攤平)、promise chaining (支撐它)

出處:第 7 段「then 也回傳 promise:鏈式處理」

undefined

未定義值

函式沒有 return 時的回傳值;在鏈式裡代表這一環沒有把結果往下傳。

影片裡第三個 then 只有 console.log 沒有 return,所以它回傳的那顆 promise 的 [[PromiseResult]] 是 undefined——注意這顆 promise 仍然是 fulfilled,只是值是 undefined,成功與有值是兩件事。實務上忘記 return 就是鏈斷掉的元凶:下一個 then 照樣會執行,只是拿到 undefined,錯誤訊息常常出現在離真正原因很遠的地方。

相關術語: [[PromiseResult]] (寫入它)、promise chaining (常見錯誤)

出處:第 7 段「then 也回傳 promise:鏈式處理」

見仁見智 5:21
「also creates a promis object and this is now set to fulfilled because we returned result time two」
結果沒錯,但時間感被壓掉了:動畫看起來像 1 → 2 → 4 在同一瞬間完成,實際上 then 回傳的那顆 promise 不是立刻 fulfilled——第一個 handler 得先被排進 microtask queue、被取出執行,回傳值才會 resolve 第二顆 promise,接著才排第二個 handler。三個 then 的鏈至少要三輪 microtask,所以與其他 microtask(例如另一條 promise 鏈)交錯時,順序會是逐輪交替而不是一條鏈先跑完。
依據: ECMA-262, 27.2.5.4 Promise.prototype.then / PerformPromiseThen 與 NewPromiseReactionJob(每個 reaction 都是一份獨立的 job)
留給下一段 零件到這裡全部到位了:state/result、reaction record、microtask 優先權、鏈式。剩下的問題是自己算不算得出來——作者接著出一道輸出順序的題目來驗收。

8. 小測驗:為什麼印出 1、3、2 6:25–8:42

作者出題:一段程式碼會依序印出 1、3、2。推演:constructor 與 executor 進 call stack,第一行 console.log(1) 立刻印出 1;resolve(2) 把 state 設為 fulfilled、result 設為 2,此時還沒有任何 fulfill reaction。下一行的 then 建立 reaction record,因為 promise 已經 resolve,這筆 record 不會被存進列表(省記憶體),但仍拿得到 result 2,於是 handler 立刻被「排進」microtask queue——排進去不等於執行。腳本還沒結束,繼續執行 console.log(3) 印出 3;腳本跑完、call stack 空了,microtask queue 裡的 handler 才被取出,印出 2。最後作者提到部落格文章、ECMAScript 規格與課程連結。

題目與答案 1、3、2 同框,是驗收自己有沒有真的理解的那一格
6:37 · 題目與答案 1、3、2 同框,是驗收自己有沒有真的理解的那一格
then 的 handler 被「排進」而非「執行」microtask queue 的畫面,正是這題的陷阱所在
7:34 · then 的 handler 被「排進」而非「執行」microtask queue 的畫面,正是這題的陷阱所在
承上 上一段收在「零件都齊了,該驗收」,這一段就是那道驗收題:一段十行的程式碼,答案是依序印出 1、3、2。

推理因為前面每一個零件都已經單獨拆解過,所以作者把它們壓縮成一道題:console.log(1) 同步印出 1 → resolve(2) 把 state 改成 fulfilled、result 設成 2,而此時 fulfill reactions 還是空的 → 下一行的 then 建立 reaction record,因為 promise 已經 resolve,這筆 record 不必存進列表(存了只是白佔記憶體),但仍拿得到 result 2,於是 handler 立刻被排進 microtask queue → script 還沒跑完,console.log(3) 印出 3 → script 結束、call stack 空了,handler 才被取出,印出 2。整道題其實只考一件事:排程不等於執行。

AI 補充陷阱正是作者特別加重語氣的那句「不是立刻被執行,而是立刻被排進 microtask queue」。截圖裡的證據很完整:主控台只有 1、microtask queue 裡躺著 handler、而 then() 還在 call stack 上——這就是「已排程、未執行」的畫面。另外「在已經 resolve 的 promise 上串 then」不是特例:只要 promise 不是 pending,then 就跳過存進列表這一步、直接排一份 job,但結論不變——仍然至少延後一輪 microtask,所以 2 絕不可能在 3 之前印出。由此可以得到一條能直接套用的規則:同一段 script 裡所有同步的 console.log 一定先印完,promise 的 then 才會開始跑,跟 resolve 得多早完全無關。最後她提到自己的文章與 ECMAScript 規格——影片裡的 [[PromiseState]]、Promise Reaction Record、job 這些名字都直接取自規格,想確認細節(例如 then 遇到已 settle 的 promise 究竟怎麼處理)讀規格的 PerformPromiseThen 一節最快。

術語:PromiseReactionJob

PromiseReactionJob

promise 反應工作

規格層面「執行一筆 reaction record 的 handler 並結束下游 promise」的那份工作,就是排進 microtask queue 的東西。

影片裡說「把 handler 排進 microtask queue」,規格的說法是建立一份 PromiseReactionJob 並交給宿主排隊。這份 job 做三件事:取出 [[PromiseResult]] 當參數、呼叫 handler、用回傳值 resolve(或用丟出的錯誤 reject)下游 promise。知道它是一份 job 就能理解為什麼鏈上每一環各花一輪 microtask——每一筆 record 都是獨立的一份 job。

相關術語: Promise Reaction Record (執行它)、microtask (就是一個)

出處:第 8 段「小測驗:為什麼印出 1、3、2」

synchronous

同步

程式碼在當前 call stack 上一路執行到底、不交給任何 queue 的執行方式。

這題的一半答案來自它:console.log(1)、resolve(2)、then() 的登記動作、console.log(3) 全部是同步的,所以 1 和 3 之間不會插進任何東西。要特別記住 resolve 是同步的——它當下就把 state 與 result 寫好了,只有 handler 的執行是非同步的。把「promise 是非同步的」理解成「promise 相關的每件事都晚一點發生」,就會答錯這道題。

相關術語: call stack (執行於)、microtask (相反)

出處:第 8 段「小測驗:為什麼印出 1、3、2」

ECMAScript specification

ECMAScript 規格

定義 JavaScript 語言行為的標準文件,影片裡的內部欄位名稱都出自它。

作者說她喜歡直接讀規格,影片的整套詞彙就是證據:[[PromiseState]]、[[PromiseFulfillReactions]]、Promise Reaction Record、job 都是規格用語,所以看懂這支影片之後讀規格會意外地順。分工要記清楚:promise 物件與 job 的語意由 ECMA-262 定義,而 job 什麼時候被跑(event loop、task queue、渲染時機)由 HTML 規格定義,Node.js 則另有自己的一套。

相關術語: internal slot (定義它)、event loop (另見 HTML 規格)

出處:第 8 段「小測驗:為什麼印出 1、3、2」

留給下一段 總結收束:Promise 的全部行為都能從三個問題推出來——物件的哪個欄位被誰改、handler 被誰排進哪個 queue、那個 queue 什麼時候被清空。

4. 總結

作者先把 new Promise 執行時生出的物件攤開:五個 internal slot,其中 state 與 result 只是被 resolve/reject 改寫的普通屬性——「nothing special here」。特別之處在被刻意跳過的 fulfill/reject reactions:then 會在那裡放一筆 promise reaction record,record 的 handler 帶著我們的 callback;resolve 的最後一步不是執行 handler,而是把帶著 result 的 handler「排進」microtask queue。為了讓「排進」有時間感,她補一段 event loop 複習——call stack 一空先清 microtask queue,才輪到 task queue。接著把玩具範例校正回實務:executor 裡該啟動一件離開主執行緒的工作,並用 setTimeout + then 一格一格走完時間線,導出「等待期間 script 仍可繼續跑」的非阻塞結論。再加一層:then 自己也回傳 promise,於是鏈式 then 能讓結果逐步加工(1 → 2 → 4)。最後用一道印出 1、3、2 的題目收攏所有零件:resolve 是同步的、handler 是被排程而非被執行、要等 call stack 空了才輪到 microtask。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
4. 複習:microtask queue 優先於 task queue
2:14
「stack is empty the event Loop first checks in the microtask que and whenever this queue is empty it goes to the task que」這個順序描述夠用來判斷輸出,但不是規格的實際流程。HTML 規格的 event loop 是「取一個 task 執行 → 立刻做一次 microtask checkpoint 把 microtask queue 清空 → 可能渲染 → 再取下一個 task」,也就是每個 task 後面都跟一次完整的 microtask 清空,而不是「microtask queue 空了才開始跑 task queue」。另外瀏覽器有多條 task queue(不同 task source)可自行挑選,Node.js 還多一條優先權更高的 process.nextTick 佇列。
依據: HTML Standard, 8.1.7.3 Event loop processing model(microtask checkpoint);Node.js docs, The Node.js Event Loop
7. then 也回傳 promise:鏈式處理
5:21
「also creates a promis object and this is now set to fulfilled because we returned result time two」結果沒錯,但時間感被壓掉了:動畫看起來像 1 → 2 → 4 在同一瞬間完成,實際上 then 回傳的那顆 promise 不是立刻 fulfilled——第一個 handler 得先被排進 microtask queue、被取出執行,回傳值才會 resolve 第二顆 promise,接著才排第二個 handler。三個 then 的鏈至少要三輪 microtask,所以與其他 microtask(例如另一條 promise 鏈)交錯時,順序會是逐輪交替而不是一條鏈先跑完。
依據: ECMA-262, 27.2.5.4 Promise.prototype.then / PerformPromiseThen 與 NewPromiseReactionJob(每個 reaction 都是一份獨立的 job)

5. 推薦三個下一步

1. 往下挖深:規格層的 job 與 promise resolve 演算法

本片把 handler 進 microtask queue 當成一個動作帶過,但規格裡它是 PromiseReactionJob,且 resolve 遇到 thenable 時還會多花一到兩輪 microtask——這正是第 7 段勘誤指出的落差。

YouTube 搜尋:PromiseReactionJob ECMAScript spec microtask checkpoint explained promise resolve thenable extra tick

2. 往旁邊對照:async/await 是同一套機制的語法糖

本片建立的執行模型只用 then 表達;async/await 背後仍是 reaction record 與 microtask queue,對照著看才知道 await 到底暫停了什麼。

YouTube 搜尋:async await event loop visualized await microtask queue JavaScript Visualized async await

3. 往上應用:組合與錯誤處理的實戰 pattern

會推單一 promise 的順序之後,真實程式碼面對的是併發與失敗:Promise.all/race/allSettled、catch 放哪一層、未處理的 rejection 怎麼冒出來。

YouTube 搜尋:Promise.all vs allSettled promise error handling patterns unhandled promise rejection

📄 全部影片 · 主題區: JavaScript 執行模型