JavaScript 是單執行緒語言,一次只能執行一個操作。如果所有事情都同步跑,任何耗時任務都會卡住整個程式——這就是事件循環(Event Loop)存在的原因,也是 setTimeout 能讓你「稍後再執行」的底層機制。
JavaScript 的單執行緒特性
單執行緒的意思是:呼叫堆疊只有一條,同時只能有一段程式碼在跑。長時間的同步操作(例如複雜運算或等待 I/O)一旦佔住堆疊,使用者介面就會完全卡死、無法回應任何輸入。
非同步程式設計的核心思路是:把等待的部分交出去,主執行緒繼續跑其他的,等結果回來再接手。
事件循環的四個組件
理解事件循環前,先認識它的基本組成:
- 呼叫堆疊(Call Stack):記錄目前正在執行的函式,採後進先出(LIFO)結構。
- Web API:瀏覽器提供的環境能力,包括計時器(
setTimeout、setInterval)、網路請求(fetch)、DOM 事件等。這些操作不在 JS 引擎內跑,而是交給瀏覽器的底層執行緒處理。 - 回呼佇列(Callback Queue / Task Queue):Web API 完成後,把回呼函式排進這裡,等待被執行。
- 事件循環(Event Loop):持續監看呼叫堆疊——只要堆疊清空,就從回呼佇列取出下一個任務推入堆疊執行。
現代 JS 還有一條優先權更高的 Microtask Queue,專門處理
Promise.then、queueMicrotask等。每次呼叫堆疊清空後,事件循環會先把 microtask queue 清空,才輪到回呼佇列。這篇聚焦setTimeout的 macrotask 行為,microtask 的細節值得另開一篇。
setTimeout 的運作流程
setTimeout(fn, delay) 被呼叫後,執行路徑如下:
- JS 引擎把計時器交給 Web API,自己繼續往下跑。
- Web API 在背景計時,計時結束後把
fn放進回呼佇列。 - 事件循環發現呼叫堆疊清空,把
fn取出推入堆疊執行。
重點是:**setTimeout 的 delay 是「最少等這麼久」,不是「剛好等這麼久」**。如果堆疊上還有大量同步程式碼在跑,回呼只能繼續等在佇列裡。
範例:setTimeout 0 為什麼還是最後跑
1 | console.log('開始'); |
輸出:
1 | 開始 |
delay 設 0 也一樣:回呼先進 Web API、再進回呼佇列,要等到呼叫堆疊完全清空才有機會執行。所以它永遠跑在當下這批同步程式碼之後。
另外,瀏覽器對 setTimeout 有一個最小延遲限制(HTML 規範定義的巢狀呼叫超過 5 層後最低 4ms),就算你寫 0,實際延遲也不會低於這個門檻。
事件循環讓 JS 能做非阻塞 I/O
靠著這個機制,JS 可以在等待網路回應、使用者點擊、計時器的同時,繼續處理其他事情,介面也不會失去回應。這就是為什麼單執行緒的 JS 在前端能應付複雜的互動場景。





