JavaScript 跑在單一執行緒上,一次只能做一件事。同步程式在呼叫堆疊上由上而下執行。任何非同步的東西——setTimeout 回呼、Promise .then、click handler——都不會就地執行,而是被放進佇列。事件迴圈就是決定「下一個跑什麼」的簡單規則:只要呼叫堆疊一空,就取出下一個等待中的任務執行,然後重複。這個工具把這個迴圈一步一步變得看得見。
其實有兩個佇列,而它們的優先順序正是整個謎題的關鍵。Promise 回呼(與 queueMicrotask)進 microtask queue;setTimeout、setInterval 與多數事件進 macrotask queue。規則是:每個 macrotask 之後(以及最初的 script 之後),事件迴圈會先清空整個 microtask queue,才執行下一個 macrotask。這就是為什麼相同延遲下,Promise .then 永遠贏過 setTimeout——microtask 會先被排乾。看這個工具裡的兩個佇列,順序就不再是謎。
關於這個主題的常見疑問與實用解答。
因為 Promise 回呼是 microtask、setTimeout 回呼是 macrotask,而事件迴圈在每個 macrotask 之後(以及最初 script 之後)會先清空整個 microtask queue,才執行下一個 macrotask。所以即使 setTimeout 設 0ms,也得在 macrotask queue 裡等,讓所有待處理的 microtask 先跑。
Promise 的反應(.then、.catch、.finally)、await 的後續、queueMicrotask 的回呼、MutationObserver 的回呼都是 microtask。setTimeout、setInterval、requestAnimationFrame、I/O 與多數 DOM 事件是 macrotask。這個區分重要,因為事件迴圈會在 macrotask 之間清空所有 microtask,所以若你無止盡地排 microtask,可能餓死畫面渲染或稍後的計時器。
不會。0ms 延遲只代表回呼「盡快可以跑」,但它仍會進 macrotask queue,等到目前的同步程式與所有待處理 microtask 都結束才執行。瀏覽器還會把巢狀計時器夾到最小值(幾層之後約 4ms),所以 0ms 是下限,不是立即呼叫。