本文 AI 產出,尚未審核

JavaScript 事件循環:同步 vs 非同步

簡介

在 JavaScript 中,同步非同步的執行方式直接影響程式的效能與使用者體驗。
同步程式碼會一行接一行、阻塞(blocking)執行;若某段程式需要等待 I/O(例如讀取檔案、發送網路請求),整個執行緒都會被卡住,導致 UI 變卡或伺服器無法即時回應。

相對地,非同步機制讓耗時的操作可以在背景完成,主執行緒得以繼續處理其他工作,這正是 事件循環(Event Loop) 發揮威力的地方。了解同步與非同步的差異、背後原理以及正確的使用方式,是成為可靠 JavaScript 開發者的必備功課。


核心概念

1. 同步執行(Blocking)

  • 定義:程式碼按照寫入順序逐行執行,前一行必須完成才能執行下一行。
  • 特性:簡單直觀,但當遇到需要等待外部資源的操作時,整個執行環境會被「卡住」。

範例 1:簡單的同步迴圈

// 計算 1~5 的總和
let sum = 0;
for (let i = 1; i <= 5; i++) {
  sum += i;          // 每次迴圈都必須等待前一次完成
}
console.log('總和 =', sum);

這段程式碼在所有迴圈結束前,console.log 不會被呼叫,執行流程完全同步。


2. 非同步執行(Non‑Blocking)

  • 定義:耗時的工作會交給瀏覽器或 Node.js 的 工作執行緒(Worker Thread)I/O 線程計時器,主執行緒在任務完成前繼續往下跑。
  • 機制:當非同步任務完成時,會把回呼函式(callback)Promiseasync/await 的後續程式碼放入 任務佇列(task queue),等到呼叫堆疊(call stack)清空後,由事件循環取出執行。

範例 2:使用 setTimeout 的非同步呼叫

console.log('A');               // 同步執行

setTimeout(() => {
  console.log('B');             // 0ms 後放入 task queue
}, 0);

console.log('C');               // 同步執行

輸出結果

A
C
B

雖然 setTimeout 的延遲時間是 0,仍然會被推到任務佇列,等到同步程式碼全部跑完才會執行。


3. Promise 與 async/await

Promise 為非同步操作提供**可鏈結(chainable)**的介面;async/await 則是語法糖,使非同步程式碼看起來像同步。

範例 3:Promise 版的 HTTP 請求(Node.js / 瀏覽器皆可)

function fetchData(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.onload = () => {
      if (xhr.status === 200) resolve(xhr.responseText);
      else reject(new Error(xhr.statusText));
    };
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.send();
  });
}

// 使用 Promise
fetchData('https://api.example.com/data')
  .then(data => console.log('取得資料:', data))
  .catch(err => console.error('錯誤:', err));

範例 4:async/await 版(更易讀)

async function loadData() {
  try {
    const data = await fetchData('https://api.example.com/data');
    console.log('取得資料:', data);
  } catch (err) {
    console.error('錯誤:', err);
  }
}
loadData();

await 會暫停 async 函式 的執行,但不會阻塞整個執行緒;底層仍然是透過 Promise 與事件循環完成。


4. 事件循環的運作流程

  1. 呼叫堆疊(Call Stack):存放同步執行的函式與程式碼。
  2. 任務佇列(Task Queue):放入已完成的非同步回呼(macro‑task),如 setTimeout、I/O 完成事件。
  3. 微任務佇列(Microtask Queue):存放 Promise 的 then/catch/finally 回呼及 queueMicrotask。微任務的優先級高於宏任務。
  4. 事件循環(Event Loop)
    • 若呼叫堆疊為空,先執行所有微任務;
    • 再從任務佇列取出最早的宏任務執行;
    • 重複此過程。

範例 5:微任務 vs 宏任務的執行順序

console.log('1️⃣ 同步開始');

setTimeout(() => console.log('2️⃣ setTimeout (macro)'), 0);
Promise.resolve().then(() => console.log('3️⃣ Promise (micro)'));

console.log('4️⃣ 同步結束');

輸出結果

1️⃣ 同步開始
4️⃣ 同步結束
3️⃣ Promise (micro)
2️⃣ setTimeout (macro)

微任務(Promise)會在同一次事件迴圈的結尾被執行,先於 setTimeout 這類宏任務。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方式
忘記回傳 Promise async 函式內部使用 await 但未回傳結果,導致呼叫端拿不到值 確保每個 async 函式都有 return,或直接回傳 Promise
過度使用 setTimeout setTimeout 模擬非同步,會造成「時間漂移」與難以除錯的程式碼 儘量使用原生 Promise、fetchfs.promises 等正式的非同步 API
微任務堆疊過深 連續大量 Promise.then 會阻塞宏任務,造成 UI 卡頓 使用 await 或將部分工作拆成 setTimeout(..., 0) 交給宏任務
錯誤未捕獲 Promise 鏈中拋出的錯誤若未 catch,會變成未處理的拒絕(unhandled rejection) 在每條 Promise 鏈最後加 .catch,或在 async 函式使用 try/catch
同步迴圈阻塞 I/O 大量計算或同步迴圈在 Node.js 中會阻塞事件循環,導致其他請求延遲 使用 worker_threadssetImmediate 或將計算切分成多段非同步執行

最佳實踐

  1. 盡量使用 Promise / async‑await:語意清晰、錯誤處理集中。
  2. 避免在主執行緒做繁重計算:可透過 Web Worker(瀏覽器)或 worker_threads(Node)分流。
  3. 善用微任務:在需要在同一次事件迴圈內完成的後續工作時,使用 Promise.resolve().then(...);但留意不要過度堆疊。
  4. 保持呼叫堆疊短小:長時間同步阻塞會讓事件循環無法執行其他任務,導致卡頓。
  5. 使用工具觀測:瀏覽器的 Performance 面板、Node 的 --trace-event 皆能幫助定位非同步瓶頸。

實際應用場景

場景 同步寫法的問題 推薦的非同步解法
前端表單提交 若直接使用 XMLHttpRequest 的同步模式,使用者在等待回應期間會無法操作頁面。 使用 fetch 搭配 async/await,提交後立即給予 UI 反馈(loading spinner)。
Node.js 讀寫檔案 fs.readFileSync 會阻塞整個伺服器,其他請求被迫排隊。 改用 fs.promises.readFilestream,讓 I/O 在背景完成。
大量資料處理 大型迴圈在瀏覽器主執行緒跑完會導致 UI 卡死。 把計算切成小塊,使用 setTimeout(..., 0)requestIdleCallback 分批執行,或交給 Web Worker。
即時聊天或推播 若使用輪詢(polling)同步請求,會浪費頻寬且阻塞。 使用 WebSocket 或 Server‑Sent Events(SSE),保持單一長連線的非同步訊息推送。
多個 API 並行呼叫 依序呼叫會造成不必要的等待時間。 使用 Promise.all 同時發起多個請求,等全部完成後一次處理結果。

總結

  • 同步是最直觀的執行方式,但會在等待 I/O 時阻塞整個執行緒,對使用者體驗與伺服器吞吐量都有負面影響。
  • 非同步透過事件循環、任務佇列與微任務機制,使耗時操作在背景完成,主執行緒得以持續回應其他事件。
  • Promiseasync/await提供了更易讀、易維護的非同步寫法,配合正確的錯誤處理與任務拆分,可大幅提升程式的可靠性與效能。
  • 理解事件循環的運作流程(宏任務 vs 微任務)是避免常見陷阱、寫出高品質非同步程式碼的關鍵。

掌握同步與非同步的差異,並善用 JavaScript 的事件循環機制,才能在前端與後端開發中寫出 流暢、可擴充且不易卡頓 的應用程式。祝你在程式之路上,玩轉非同步,寫出更好的 JavaScript!