JavaScript 事件循環是該語言中最重要但又被誤解的概念之一。儘管 JavaScript 是單線程的,但它的事件循環通過複雜的隊列系統實現了非阻塞並發。本文建構了 JavaScript 如何處理非同步執行的完整心智模型,從呼叫堆疊到微任務佇列以及其間的所有內容。
呼叫堆疊和執行模型
JavaScript 使用具有 LIFO(後進先出)結構的呼叫堆疊。當一個函數被呼叫時,一個新的堆疊幀被壓入堆疊中。當函數返回時,框架就會彈出。關鍵原則是運行到完成:每個函數在任何其他程式碼可以中斷它之前運行完成。
function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function logSquare(x) { console.log(square(x)); }
logSquare(4); // call stack: logSquare → square → multiply → ...
這種單執行緒模型意味著長時間運行的操作會阻塞主執行緒。事件循環透過將等待操作委託給運行時環境(瀏覽器或 Node.js)並透過專門的佇列非同步處理它們的回調來解決這個問題。
巨集任務隊列
巨集任務,也稱為任務,源自 Node.js 中的 setTimeout、setInterval、I/O 事件、UI 事件和 setImmediate。事件循環在每次迭代中從佇列中選取一個巨集任務,執行它直至完成,然後在移動到下一個巨集任務之前處理微任務佇列。
console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
console.log("End");
// Output: Start, End, Timeout
即使延遲為 0,setTimeout 回呼也會作為巨集任務排隊,並且僅在目前同步程式碼和所有微任務完成後才執行。這種行為經常被希望 setTimeout(fn, 0) 立即執行的開發人員誤解。
微任務佇列
微任務比巨集任務具有更高的優先權。它們源自於 Node.js 中的 Promise .then()、.catch()、.finally()、queueMicrotask()、MutationObserver 和 process.nextTick()。關鍵規則是在處理下一個巨集任務之前,整個微任務佇列都會被清空。
console.log("1");
Promise.resolve().then(() => console.log("2"));
setTimeout(() => console.log("3"), 0);
console.log("4");
// Output: 1, 4, 2, 3
這種順序解釋了為什麼 Promise 在 setTimeout 回呼之前解析,即使兩者都準備好了。一個危險的後果是微任務飢餓:遞歸調度微任務可能會阻止巨集任務執行,從而凍結應用程式。
| 佇列類型 | 來源 | 優先 | 每個週期排空 |
|---|---|---|---|
| 巨集任務 | setTimeout、I/O、UI 事件 | 降低 | 一項任務 |
| 微任務 | Promise、queueMicrotask | 更高 | 整個佇列 |
| 渲染 | 請求動畫幀 | 之間 | 塗漆前 |
非同步/等待內部結構
async 和 await 關鍵字是 Promise 和生成器函數的語法糖。非同步函數同步執行到第一個 await 表達式,此時它將控制權交還給事件循環,並在等待的 Promise 解析為微任務時恢復。
async function example() {
console.log("A");
await Promise.resolve();
console.log("B");
}
example();
console.log("C");
// Output: A, C, B
await 關鍵字不會阻塞執行緒。它脫糖為 Promise .then() 回調,該回呼作為微任務排隊。這就是為什麼 await 之後的程式碼在所有同步程式碼完成後執行。
requestAnimationFrame 和渲染
渲染管道(樣式重新計算、佈局和繪製)發生在事件循環週期中的特定積分。 requestAnimationFrame 回呼在瀏覽器執行繪製之前執行,使它們成為視覺更新的正確 API。
function animate(timestamp) {
element.style.transform = `translateX(${Math.sin(timestamp / 1000) * 100}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
對動畫使用 setTimeout 或 setInterval 會導致卡頓,因為它們的回呼與瀏覽器的繪圖週期不一致。另一方面,requestIdleCallback API 是為不應該影響幀預算的非關鍵工作而設計的。
性能影響
長任務(JavaScript 執行超過 50 毫秒)會阻塞主執行緒並導致明顯的卡頓。 Long Tasks API 支援程式監控:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log("Long task detected:", entry.duration, "ms");
}
});
observer.observe({ entryTypes: ["longtask"] });
長任務直接影響核心網路生命,特別是與下次繪製的互動(INP)。緩解策略包括使用 setTimeout 或 scheduler.yield() 分解長任務、將繁重的計算卸載給 Web Workers,以及使用 requestIdleCallback 進行可推遲的工作。
結論
掌握事件循環對於編寫高效能 JavaScript 至關重要。了解呼叫堆疊、巨集任務佇列、微任務佇列和渲染管道之間的關係有助於您避免常見錯誤、最佳化動畫效能並診斷生產問題。具有優先隊列排出的運行到完成執行的思維模型是所有非同步 JavaScript 開發的基礎。

