本文 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)、Promise 或 async/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. 事件循環的運作流程
- 呼叫堆疊(Call Stack):存放同步執行的函式與程式碼。
- 任務佇列(Task Queue):放入已完成的非同步回呼(macro‑task),如
setTimeout、I/O 完成事件。 - 微任務佇列(Microtask Queue):存放 Promise 的
then/catch/finally回呼及queueMicrotask。微任務的優先級高於宏任務。 - 事件循環(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、fetch、fs.promises 等正式的非同步 API |
| 微任務堆疊過深 | 連續大量 Promise.then 會阻塞宏任務,造成 UI 卡頓 |
使用 await 或將部分工作拆成 setTimeout(..., 0) 交給宏任務 |
| 錯誤未捕獲 | Promise 鏈中拋出的錯誤若未 catch,會變成未處理的拒絕(unhandled rejection) |
在每條 Promise 鏈最後加 .catch,或在 async 函式使用 try/catch |
| 同步迴圈阻塞 I/O | 大量計算或同步迴圈在 Node.js 中會阻塞事件循環,導致其他請求延遲 | 使用 worker_threads、setImmediate 或將計算切分成多段非同步執行 |
最佳實踐
- 盡量使用 Promise / async‑await:語意清晰、錯誤處理集中。
- 避免在主執行緒做繁重計算:可透過
Web Worker(瀏覽器)或worker_threads(Node)分流。 - 善用微任務:在需要在同一次事件迴圈內完成的後續工作時,使用
Promise.resolve().then(...);但留意不要過度堆疊。 - 保持呼叫堆疊短小:長時間同步阻塞會讓事件循環無法執行其他任務,導致卡頓。
- 使用工具觀測:瀏覽器的 Performance 面板、Node 的
--trace-event皆能幫助定位非同步瓶頸。
實際應用場景
| 場景 | 同步寫法的問題 | 推薦的非同步解法 |
|---|---|---|
| 前端表單提交 | 若直接使用 XMLHttpRequest 的同步模式,使用者在等待回應期間會無法操作頁面。 |
使用 fetch 搭配 async/await,提交後立即給予 UI 反馈(loading spinner)。 |
| Node.js 讀寫檔案 | fs.readFileSync 會阻塞整個伺服器,其他請求被迫排隊。 |
改用 fs.promises.readFile 或 stream,讓 I/O 在背景完成。 |
| 大量資料處理 | 大型迴圈在瀏覽器主執行緒跑完會導致 UI 卡死。 | 把計算切成小塊,使用 setTimeout(..., 0) 或 requestIdleCallback 分批執行,或交給 Web Worker。 |
| 即時聊天或推播 | 若使用輪詢(polling)同步請求,會浪費頻寬且阻塞。 | 使用 WebSocket 或 Server‑Sent Events(SSE),保持單一長連線的非同步訊息推送。 |
| 多個 API 並行呼叫 | 依序呼叫會造成不必要的等待時間。 | 使用 Promise.all 同時發起多個請求,等全部完成後一次處理結果。 |
總結
- 同步是最直觀的執行方式,但會在等待 I/O 時阻塞整個執行緒,對使用者體驗與伺服器吞吐量都有負面影響。
- 非同步透過事件循環、任務佇列與微任務機制,使耗時操作在背景完成,主執行緒得以持續回應其他事件。
- Promise與async/await提供了更易讀、易維護的非同步寫法,配合正確的錯誤處理與任務拆分,可大幅提升程式的可靠性與效能。
- 理解事件循環的運作流程(宏任務 vs 微任務)是避免常見陷阱、寫出高品質非同步程式碼的關鍵。
掌握同步與非同步的差異,並善用 JavaScript 的事件循環機制,才能在前端與後端開發中寫出 流暢、可擴充且不易卡頓 的應用程式。祝你在程式之路上,玩轉非同步,寫出更好的 JavaScript!