跳至主要内容

Day 19: 原理篇 —— 為什麼程式不乖乖排隊?JS 腦袋裡的 Event Loop

在前端開發中,最令人困惑的現象之一莫過於:「明明在第一行寫了 setTimeout(fn, 0),為什麼它卻在最後一行的同步代碼之後才執行?」本章節將帶領冒險者潛入 JavaScript 引擎的思維深處。我們將揭開 JavaScript 作為「單執行緒(Single Thread)」單線跑者的運作真相,深入剖析 Call Stack(呼叫堆疊)、Web APIs(瀏覽器代管區)與 Task Queue(任務排隊佇列)如何透過「Event Loop(事件循環)」精密協作,並探討主執行緒阻塞(Blocking)的成因與防範之道。

本文同步分享於2026鐵人賽:JS 核心重構:勇者轉職傳說

📜 這裡的修煉規則

孩子,在踏上今日的領地前,先坐下來喝杯酒吧。 修煉的路很長,別急著奔跑,先聽聽這份卷軸裡的古老叮嚀。

我在每一站的盡頭都為你鋪設了 實戰演武場 CodePen。那不只是程式碼的拼湊,而是你在這場暴風雨中,試圖用邏輯點燃的一顆星火。 成功突圍後,別忘了帶著你的戰利品回到 QuestBoard 佈告欄。在那裡,你會發現自己從不孤單,公會的夥伴們都正舉杯等待你的歸來。

記住...

導師看重的從來不是你的劍法有多華麗,而是當你被邏輯擊潰、滿身泥濘後,依然選擇握緊理性的劍柄,再次向真相發起衝鋒的勇氣。

🛡️ 【公會大廳:導師的修煉導讀】

「導師,這不可能!我明明在第一行寫了 setTimeout(..., 0),為什麼它卻在我最後一行的 console 之後才印出來?0 秒難道不是立刻嗎?」

我放下手中的羽毛筆,看著眼前焦慮的新手冒險者。

「孩子,這就是 JavaScript 最誠服也最神祕的本質。它是一個 『單線跑者』。即便你給他一百個任務,他的大腦一次也只能思考一件事。今天,我們要潛入 JS 的思維深處,看看那位忙碌的 『辦事員』 是如何與 『自動販賣機 (Web APIs)』 以及 『排隊區 (Queue)』 協作,才不至於讓整個網頁崩潰。」

這章節,我們不學語法,我們要學「看清時間的流向」。


💡 【導師講義:底層真相探究】

1. 單線跑者:一次只能做一件事

JavaScript 的核心引擎(Call Stack,呼叫堆疊)就像一個狹窄的單向服務櫃檯。不論是數學運算、選取 DOM 節點、還是執行迴圈,辦事員一次只能處理一個動作。

如果遇到耗時的任務(例如:向遠端伺服器請求巨大資料卷宗),若辦事員停在原地乾等,整個公會大廳(網頁 UI)就會完全凍結,使用者連按鈕都無法點擊。

2. 時空分身:辦事員、自動販賣機與排隊區

為了不讓主執行緒卡死,JavaScript 與瀏覽器發展出了一套精妙的協作機制:

  1. 櫃檯 (Call Stack):正在處理的當前同步任務。
  2. 自動販賣機 (Web APIs):計時器(setTimeout)、網路請求(fetch)、DOM 事件監聽等由瀏覽器代管的非同步操作。
  3. 排隊區 (Task Queue):當 Web APIs 完成等待或事件觸發後,將回呼函式放入此處排隊等待結算。
🏹 導師的辨析:執行順序與三大空間對照
動作代碼類型處理空間執行時機
console.log()同步任務Call Stack (櫃檯)立即在主線程執行
setTimeout()非同步任務Web APIs ➡️ Task Queue丟給瀏覽器計時,時間到進排隊區
超大 for 迴圈 (百萬次)阻塞任務Call Stack (櫃檯)強佔櫃檯,算完前阻斷一切後續操作

3. 本日圖解心法:單手辦事員與 Event Loop 哨兵

🛖 營火叮嚀:導師的圖解心法

想像辦事員只有一隻手。當他遇到 setTimeout 時,會先把計時任務丟給旁邊的「Web APIs 自動販賣機」代管。只有當他的櫃檯 完全清空 (Call Stack Empty) 時,巡邏哨兵(Event Loop)才會轉頭看向「Task Queue 排隊區」,把排在第一位的任務拉回櫃檯執行。這就是為什麼 setTimeout(..., 0) 依然必須乖乖排隊!

Event Loop 原理圖


⚔️ 【戰術對抗:學長與學弟的代碼對決】

招式示範:預測非同步輸出順序

execution-order.js
console.log("A: 開始修煉"); // 同步任務

setTimeout(() => {
console.log("B: 計時炸彈爆了"); // 非同步回呼
}, 0);

console.log("C: 結束修煉"); // 同步任務

❌ 冒險學弟:直覺式解讀 (以為代碼會單純由上而下執行)

  • 錯誤直覺輸出順序A ➡️ B ➡️ C
  • 迷思陷阱:誤以為 0 毫秒等於「插隊立即執行」。

✅ 勇者學長:底層式解讀 (看清排隊機制的真身)

  • 正確輸出順序A ➡️ C ➡️ B
  • 利潤評級:🟢 掌握 Event Loop 核心調度邏輯
💡 導師講評:為什麼 C 會比 B 還快?

因為 B 在一開始就被送進了 Web APIs 計時區,即便計時為 0 秒,回呼函式也只能被推入 Task Queue 排隊。辦事員必須先將 Call Stack 中剩餘的同步任務 C 執行完畢,讓櫃檯徹底清空,Event Loop 才會把 B 提審到櫃檯執行。


🏰 【勇者精英課:邁向職人的進階架構】

阻塞 (Blocking) 的大危機

⚠️ 實戰雷區:避開那些致命陷阱

如果你在主線程中執行了超龐大的計算(例如 for (let i = 0; i < 1000000000; i++)),辦事員將會在 Call Stack 中死命運算。這段期間內:

  1. Event Loop 無法檢查 Task Queue。
  2. 瀏覽器無法重新繪製(Repaint)畫面。
  3. 所有使用者的點擊、捲動事件全部停擺,瀏覽器將跳出「網頁沒有回應」的警告!

職人金句: 絕不要在主櫃檯霸佔過久的同步重體力活,學會把重型任務拆解、或善用非同步與 Web Worker 進行背景分流。


🛖 營火叮嚀:導師的經驗談

孩子,導師當年剛入行時,以為 setTimeout(fn, 1000) 代表「精準在 1.000 秒後執行」。

結果有一次我的 Call Stack 正忙著處理一個巨大的同步圖形計算,耗時了 3 秒,導致原本預約 1 秒的計時器直到第 4 秒才被印出來。

記住:JavaScript 中的計時延遲不是「精準執行的約定」,它只是「最快何時能被推入排隊區」的最低下限保證。

天色微亮,營火雖已燃盡,但你眼中卻閃爍著領悟的光芒。在前往演武場挑戰關卡前,我已經幫你把靈魂碎片精煉成了這份「戰術錦囊」。拿好它,今日的任務不再是負擔,而是你證明自我的舞台。去吧,公會的英雄榜在等著你的戰報。

📝 【夥伴筆記:今日修煉精華】

這份筆記是你的隨身護身符,卡關時看一眼,真相就在裡面。

  • Single Threaded (單執行緒):JavaScript 大腦一次只能處理單一同步任務。
  • Call Stack (呼叫堆疊):當前正在執行的工作櫃檯,所有同步程式碼都在此執行。
  • Web APIs (瀏覽器代管區):負責處理計時、DOM 事件與 AJAX 網路請求等耗時等待。
  • Task Queue (任務佇列):非同步事件完成後,等待回歸 Call Stack 的排隊區。
  • Event Loop (事件循環):負責巡邏並在 Call Stack 清空時,依序將 Task Queue 中的任務拉入櫃檯。

🎯 【實戰演武場】

⚔️ 任務鑑定條件:

  1. 完成 📜 本日實戰任務 (CodePen)
  2. 將 CodePen 網址貼至 QuestBoard,並回填鑑定報告:
    • 初心者:時空裂縫預測 (成就感發掘)
        1. 如果你在 setTimeout 裡面再寫一個 setTimeout,誰會先跑?
    • 冒險者:阻塞的代價 (理性挑戰)[⚡ 觀念辨析]
        1. 你能理解為什麼 alert() 會讓整個網頁的所有計時器都暫停嗎?(因為它強佔了哪一個區塊?)
  3. 任務完成後,你的名字將永遠標記在公會的英雄榜上!

📚 【圖書館卷軸與前後站連結】