JavaScript 系列 EP.04

Event Loop:
單執行緒的異步奇蹟

為什麼 JS 只有一個執行緒卻不會卡死?深入理解 Call Stack、Task Queue 與 Microtasks 的協作模式,掌握前端效能優化的核心。

Non-Blocking
Asynchronous

痛點:為什麼我的按鈕沒反應?

你是否遇過這樣的情況:網頁在執行一段複雜的運算時,點擊按鈕沒有反應、動畫卡住、甚至瀏覽器跳出「網頁沒有回應」? 這是因為 JavaScript 是單執行緒 (Single-threaded)

JS 引擎的限制

JS 引擎(如 Chrome 的 V8)只有一個 **Call Stack**。同一時間只能做一件事。 如果你在 Stack 裡塞進一個執行 5 秒鐘的 `while(true)`,那這 5 秒內,瀏覽器完全無法處理點擊事件或更新畫面。

// 🚫 阻塞主執行緒的罪魁禍首

while (isComputing) {

// 做極其複雜的數學運算...

}

一、視覺化 Event Loop:四大主角

為了解決單執行緒的阻塞問題,JS 環境(瀏覽器或 Node.js)提供了一套機制。讓我們看看這四個組件是如何運作的:

Call Stack

正在執行中的同步程式碼。

Web APIs

處理計時器、DOM 事件、AJAX。

Microtasks

Promise.then、MutationObserver (優先級高)。

Task Queue

setTimeout、setInterval (優先級低)。

Event Loop 的工作只有一件事:

「如果 Call Stack 是空的,就去 Microtask Queue 搬東西過來跑;如果 Microtask 也空了,再去 Task Queue 搬東西。」

二、微任務 vs 宏任務:誰先執行?

這是最經典的面試題。你需要記住:微任務 (Microtasks) 永遠插隊在宏任務 (Macrotasks) 之前。

Macrotasks (宏任務)
  • setTimeout / setInterval
  • setImmediate (Node.js)
  • I/O 操作
  • UI 渲染 (Rendering)
Microtasks (微任務)
  • Promise.then / .catch / .finally
  • Async / Await (本質是 Promise)
  • process.nextTick (Node.js 優先級最高)
  • MutationObserver

測驗:這段程式碼會印出什麼?

event_loop_quiz.js
console.log("1. Start");

setTimeout(() => {
  console.log("2. Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("3. Promise");
});

console.log("4. End");

答案:1 -> 4 -> 3 -> 2

解析:1 和 4 是同步程式碼先跑。3 是微任務,在同步跑完後立即執行。2 是宏任務,排在最後。

三、Async / Await:優雅的包裝

`async/await` 讓異步看起來像同步。但它背後依然是 Promise。當執行到 `await` 時,函式會**暫停**,交出執行權,並把剩餘的部分塞進 Microtask Queue。

Async 流程解析
async function myFunc() {
  console.log("Inside - Start");
  await Promise.resolve(); // 這裡會暫停
  console.log("Inside - End"); // 這裡被塞進微任務隊列
}

console.log("Global - Start");
myFunc();
console.log("Global - End");
印出順序:Global - Start -> Inside - Start -> Global - End -> Inside - End

四、效能實戰:不要阻塞渲染

瀏覽器通常每 16.7 毫秒 (60fps) 會進行一次 **Render (渲染)**。如果你的 JS 執行時間太長,超出了這個視窗,使用者就會感覺到掉幀。

壞習慣:長任務

一次性處理 10 萬筆資料,導致 Stack 一直是滿的,瀏覽器無法進行 Render。

好習慣:任務切分

使用 `setTimeout` 或 `requestIdleCallback` 將大任務拆成小碎片,讓 Render 有機會插隊進來。

優雅處理大量數據
function processInChunks(items) {
  if (items.length === 0) return;

  // 處理 100 筆
  const chunk = items.splice(0, 100);
  doWork(chunk);

  // 將剩下的 100 筆排入下一個 Task,給瀏覽器「喘息 (渲染)」的機會
  setTimeout(() => processInChunks(items), 0);
}

總結:理解規則,優化體驗

Event Loop 是 JavaScript 的「調度員」。理解它的運作邏輯,不僅能幫你通過面試,更能讓你在面對複雜的異步邏輯與效能瓶頸時,清楚知道問題出在哪裡,並寫出更流暢、更穩定的前端應用。

#JavaScript
#EventLoop
#Async
#Performance
#V8
#Microtasks
#Promise