Promise 的執行模型:物件內部欄位、promise reaction record 與 microtask queue
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(8)
1. Outline
- 起點 · 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——答得出來就代表整條執行模型真的看懂了。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- Promise 不可怕,只是看不見底層 → 既然要看「引擎到底做了什麼」,第一個必須回答的問題就是:new Promise 執行的那一瞬間,記憶體裡到底生出了什麼東西?
- new Promise 建了什麼物件 → 五個欄位裡有兩個(fulfill reactions、reject reactions)被作者刻意跳過,而且他還特別說「到這裡沒什麼特別的」——那 promise 特別的地方必然就在那兩個空欄位裡,下一段要問的是:誰會把東西放進去?
- then 建立 promise reaction record → handler 已經被放進 microtask queue 了,但「放進 queue」到底什麼時候會被拿出來執行?要判斷輸出順序,得先講清楚 queue 之間的優先權。
- 複習:microtask queue 優先於 task queue → 執行順序的判準有了,但目前所有範例的 resolve 都是同步寫在 constructor 裡的假動作——下一段得先把場景校正回真實用法:executor 裡到底該放什麼?
- executor 裡真正該放的東西 → 場景校正完了,接下來就把「constructor 裡放一個 setTimeout、後面接一個 then」這段最接近真實的程式碼,一格一格走完整條時間線。
- setTimeout + then 的逐格執行 → 這條時間線只處理了一個 then、一次結果。如果我想把結果再加工一次呢?線索是作者到目前為止刻意沒提的一件事:then 本身也有回傳值。
- then 也回傳 promise:鏈式處理 → 零件到這裡全部到位了:state/result、reaction record、microtask 優先權、鏈式。剩下的問題是自己算不算得出來——作者接著出一道輸出順序的題目來驗收。
- 小測驗:為什麼印出 1、3、2
3. 逐段說明
JavaScript Visualized - Promise Execution
1. Promise 不可怕,只是看不見底層 0:00–0:19
作者開場說 Promise 常讓人覺得嚇人、煩躁,但那是因為看不到它在底下做了什麼。這支影片要做的事就是把「跟 Promise 互動時,引擎到底發生什麼」畫出來。定調:理解 Promise 不是背 API,而是看懂執行過程。
推理因為觀眾對 Promise 的抗拒來自「看不見底下發生什麼」,所以作者不從語法教起,而是先把問題重新定義成一個可視化問題——只要把引擎在跟 promise 互動時做的事畫出來,那種複雜感就會消失。她用「I promise you」開玩笑,同時把整支影片的題目定成 promise execution(執行過程),不是 promise usage(用法)。
AI 補充讓人覺得 daunting 的通常不是 then/catch 的寫法,而是三件看不見的事:promise 物件自己的狀態、我們傳進去的 callback 被存到哪裡、以及它到底什麼時候被拿出來跑。這支影片的路線正好照這三件事的順序走,所以接下來看到的第一件東西會是「記憶體裡的物件長什麼樣」,而不是「怎麼寫 async/await」。先建立這個期待,後面每一格動畫才知道要看哪裡。
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 重新定義成「看得見執行過程就不難」,所以作者接著做最基礎的一步:呼叫 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,我們只是呼叫函式去改物件屬性」,好讓下一段的反差成立。
- Rnew Promise 建出的物件有五個 internal slot:state、result、兩種 reactions、is handled→ 存+回想
- Cexecutor 是傳給 new Promise 的 (resolve, reject) => {},同步執行,負責啟動工作並決定結果→ 畫地圖
- E勘誤:resolve/reject 是 constructor 造出來傳進 executor 的,不是 executor 提供的→ 存+演練
- R狀態只能單向轉一次:pending → fulfilled 或 rejected,之後再呼叫 resolve/reject 都沒效→ 存+回想
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
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——非同步的部分從這裡才開始。


推理因為上一段刻意證明了 resolve 只是改屬性、沒什麼特別,所以作者接著把落差補上:then 的職責是建立一筆 promise reaction record 放進 fulfill reactions,record 裡的 handler 包著我們傳給 then 的那段 callback。於是 resolve 的動作多出了一步——寫完 state 與 result 之後,把拿到 result 的 handler 排進 microtask queue。她在這裡明確宣告:非同步是從這一步才開始的。
- Cthen 的職責是建立一筆 reaction record 放進 fulfill reactions,handler 包著我們的 callback→ 畫地圖
- Athen 只負責「登記」,觸發由 resolve 負責,執行時機由 event loop 決定——三件事分開看→ 批判類比
- R[[PromiseIsHandled]]:串上 then/catch 才變 true;rejected 沒人接手就靠它判 unhandled→ 存+回想
- Rfulfill reactions 是列表不是單格:同一顆 promise 可以串好幾個 then,每個放一筆→ 存+回想
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]]
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 有優先權。

推理因為上一段把非同步的起點交給了 microtask queue,所以作者插入一段 event loop 快速複習:call stack 一空,event loop 先看 microtask queue,等它清空才輪到 task queue(也有人叫 callback queue 或 macrotask queue)。她只要求記住一句「microtask queue 有優先權」,因為後面兩個範例的輸出順序全靠這一句判定。
- Cmicrotask queue 放 promise handler,call stack 一空就先清它,清空才輪到 task queue→ 畫地圖
- Cevent loop:不斷檢查 call stack 空了沒、哪個 queue 有東西,把工作搬上 call stack→ 畫地圖
- E更精確的模型:每跑完一個 task 就把 microtask queue 整個清空,清空中新產生的 microtask 同輪繼續跑→ 存+演練
- R四個框:call stack 現在跑的、Web APIs 環境在等的、task queue 放 setTimeout、microtask 放 handler→ 存+回想
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 更優先。
「stack is empty the event Loop first checks in the microtask que and whenever this queue is empty it goes to the task que」
5. executor 裡真正該放的東西 2:30–3:02
前面為了簡化,resolve 和 reject 都是在 constructor 裡同步呼叫的。實務上會在 executor 裡啟動非同步工作——任何離開主執行緒的事情:讀檔案、網路請求、甚至只是一個 timer。等它們回傳資料,就在它們的 callback 裡 resolve 資料、或在出錯時 reject。
推理因為前面為了畫圖方便都同步呼叫 resolve,所以作者在進入完整推演之前先把 executor 的正確用法講清楚:在裡面啟動一件離開主執行緒的工作——讀檔案系統、發網路請求、或單純只是一個 timer——等它的 callback 帶著資料回來,再用資料 resolve,或在出錯時 reject。這樣後面那條時間線才有真實意義。
- Casynchronous task:任何離開主執行緒、交給執行環境去做的事——讀檔、網路請求、timer→ 畫地圖
- Ppromisify:把 callback 式 API 包進 new Promise,在 callback 裡 resolve/reject→ 練習
- RPromise 不會讓程式變多執行緒、不會讓同步重運算變快;只是把「等的工作何時完成」包成可掛 handler 的物件→ 存+回想
AI 補充「離開主執行緒」的實際意思是:那件事不是由 JavaScript 引擎自己算,而是交給執行環境(瀏覽器或 Node)去做,所以 JS 這條執行緒可以繼續往下跑。這同時界定了 Promise 不做什麼——它不會讓程式變成多執行緒,也不會讓同步的重運算變快;它只是把「一件正在等的工作何時完成」包成一個可以掛 handler 的物件。所以 new Promise 幾乎總是出現在「把舊的 callback 式 API 包起來」的場合(fs.readFile、XMLHttpRequest、setTimeout),這個動作一般叫 promisify。反過來說,如果 executor 裡完全沒有非同步工作,那顆 promise 只是繞了一圈的同步值,除了強迫結果延後一輪 microtask 之外沒有任何好處。
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 可以繼續跑、保持互動,這就是非阻塞。



推理因為前面三段已經分別建立了物件欄位、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 一直可以繼續跑、保持互動,這就是非阻塞。
- P逐格推演 setTimeout + then 的時間線:用四個框追蹤每一步誰進 call stack、誰排進哪個 queue→ 練習
- E時間線裡有兩次「等 call stack 空」:一次讓 setTimeout callback 進來,一次讓 handler 進來,少算一次就算錯順序→ 存+演練
- Cnon-blocking 分兩層:等待本身由環境等、不佔 JS 執行緒;結果處理排進 microtask queue、不插隊打斷正在跑的程式→ 畫地圖
- RsetTimeout 的毫秒數是「最早何時可以進 task queue」,不是「何時執行」,延遲永遠只是下限→ 存+回想
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
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 非阻塞地一步步處理。


推理因為上一段的推演停在單一 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。她再把數字換成「圖片先縮放、再加濾鏡、再換格式」,說明鏈式的真正用途是逐步加工。
- Cthen 回傳一顆新 promise,handler 的回傳值就是那顆 promise 的 result,所以能串成鏈逐步加工→ 畫地圖
- E1 → 2 → 4 看起來一瞬間算完,其實三個 then 花三輪 microtask,鏈越長結果越晚出現→ 存+演練
- R忘記 return 是鏈式最常見的 bug:下游 then 拿到 undefined→ 存+回想
- Rhandler 回傳一顆 promise(或 thenable)時,下游不會拿到「一顆 promise」,而是等它 settle 後取值→ 存+回想
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
「also creates a promis object and this is now set to fulfilled because we returned result time two」
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 規格與課程連結。


推理因為前面每一個零件都已經單獨拆解過,所以作者把它們壓縮成一道題: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。整道題其實只考一件事:排程不等於執行。
- P推輸出順序的規則:同步 console.log 一定先印完,promise 的 then 才開始跑,跟 resolve 得多早無關→ 練習
- E小測驗印出 1、3、2:resolve(2) 同步改狀態,then 立刻排進 microtask queue,但 3 先印,script 結束後 2 才印→ 存+演練
- R在已經 settle 的 promise 上串 then:跳過存進列表,直接排一份 job,但仍至少延後一輪 microtask→ 存+回想
- R影片裡 [[PromiseState]]、Promise Reaction Record、job 這些名字直接取自 ECMAScript 規格→ 存+回想
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
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
- 接著看 ←9 JavaScript Concepts That Got Me To Senior Dev · 九概念站建立了 call stack、event loop、callback,這站把 Promise 拆到欄位層
- 接著看 ←Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer) · 面試題站點到 microtask queue 就停;這站補上 handler 何時被排進去
- 相關 —Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal · Promise 一次一個值,RxJS 把同一個非同步問題換成多值的 stream
- 接著看 →JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue · 作者明說先看 promise 那支;懂了 reaction record 才接得住 microtask queue 的優先權
- 相關 —JavaScript Visualized - Closures · 同系列視覺化,都把 execution context 拆開看內部欄位,先看誰都行
- 接著看 ←JavaScript Visualized - Execution Contexts · execution context 與 call stack 建立後,那站看 Promise 怎麼排進微任務佇列
- 相關 —Map, switchMap, mergeMap, flatMap, concatMap, exhaustMap in RxJS - what is the difference? · 同樣在講多個非同步工作的排隊與競態,一個用 Promise、一個用 Observable