Joseph Chen
一、JS 是單執行緒,卻能「同時」做很多事
JavaScript 是單執行緒(Single-threaded)語言,也就是說, 它一次只能做一件事。但我們天天寫 setTimeout、fetch、addEventListener——這些明明看起來是「同時在跑」的,這是怎麼做到的?
答案是:JS 本身確實只有一條執行緒,但它所在的宿主環境(瀏覽器或 Node.js) 提供了額外的能力——Web APIs。這些 API 可以在背景執行耗時操作, 完成後把結果透過 Event Loop 通知 JS。
JS Runtime 架構示意
JS Engine(V8)
Web APIs
Event Loop
Web APIs 完成後把 callback 放進 Queue,Event Loop 看到 Call Stack 空了就把 Queue 裡的任務搬進來執行
二、Call Stack:JS 的執行順序管理員
Call Stack(呼叫堆疊)是 JS 追蹤「現在執行到哪個函式」的資料結構, 遵循後進先出(LIFO)原則——最後呼叫的函式最先結束。
Maximum call stack size exceeded。 這就是 Stack Overflow 的由來(也是那個著名 QA 網站的名稱來源)。三、Macrotask vs Microtask:兩種任務佇列
非同步任務完成後,callback 會被放進「佇列(Queue)」等待執行。 但佇列不只一個——JS 有兩種優先級不同的佇列:
Microtask Queue
微任務佇列
每次 Call Stack 清空後,必須把 Microtask Queue 全部清空,才能執行下一個 Macrotask。
屬於 Microtask 的 API
Macrotask Queue
宏任務佇列(Task Queue)
每次只取出一個 Macrotask 執行,執行完後再去清空 Microtask Queue,如此循環。
屬於 Macrotask 的 API
Event Loop 的完整循環邏輯(虛擬碼)
四、互動圖解:這段程式碼的執行順序
很多人以為 setTimeout 0 等於「立刻執行」, 但實際上 promise 永遠比 timeout 先印出來。 下面的互動圖解一步步說明原因:
步驟 1 / 6
開始執行
同步程式碼進 Call Stack 執行
Call Stack
Microtask Queue
(空)
Macrotask Queue
(空)
目前輸出
五、Event Loop 常見陷阱
setTimeout 0ms 不是「立刻執行」
setTimeout(fn, 0) 只是說「把 fn 放進 Macrotask Queue, 盡快執行」。但它必須等到 Call Stack 清空、Microtask Queue 也清空之後才會執行。0ms 只是最短等待時間,不是實際執行時間。
無限 Microtask 會阻塞頁面渲染
瀏覽器的畫面重繪(re-render)是在 Macrotask 之間執行的。 如果 Microtask Queue 一直有任務(例如 then 裡面不斷 resolve 新 Promise), 畫面就永遠不會更新——頁面「卡死」。
長時間同步運算阻塞 UI
JS 是單執行緒,同步程式碼執行期間,Event Loop 完全停擺——使用者的點擊、滾動、動畫全部凍結。
六、async/await 底層就是 Promise + Microtask
async/await 是 Promise 的語法糖。await 的本質是:暫停函式執行,把後面的程式碼包進 .then(), 放進 Microtask Queue 等待。
核心重點整理
Call StackJS 執行同步程式碼的地方,LIFO,一次只跑一個函式
Web APIs瀏覽器提供,在背景執行 setTimeout、fetch 等,完成後把 callback 放進 Queue
Microtask QueuePromise.then 的 callback,Call Stack 清空後立刻全部清空(優先於 Macrotask)
Macrotask QueuesetTimeout / 事件 callback,每次只取一個執行,執行完先清空 Microtask
Event Loop不停地問:Call Stack 空了嗎?空了就先清 Microtask,再取一個 Macrotask
setTimeout 0不是立刻執行,是「Call Stack 和 Microtask 都清空後」才執行
async/awaitawait 暫停函式,把之後的程式碼丟進 Microtask Queue,本質是 Promise.then