你是否曾經想過為什麼某些 JavaScript 程式碼似乎無法按順序運行?理解這一點的關鍵是 事件循環。
JavaScript 的事件循環可能難以理解,特別是在處理不同類型的非同步操作時。在本文中,我們將解析 JavaScript 如何處理 同步 和 非同步 程式碼、微任務 和 巨任務,以及為什麼某些事情會以特定的順序發生。
JavaScript 主要以兩種方式處理操作:同步 和 非同步。理解它們之間的區別是掌握 JavaScript 如何處理任務及如何編寫高效且非阻塞程式碼的關鍵。
同步程式碼是 JavaScript 的預設行為,意味著每一行程式碼依序運行。例如:
console.log("第一");
console.log("第二");
這將輸出:
第一
第二
非同步程式碼允許某些任務在背景運行並稍後完成,而不會阻塞其餘程式碼。像 setTimeout() 或 Promise 的函數都是非同步程式碼的範例。
這是一個使用 setTimeout()
的非同步程式碼簡單範例:
console.log("第一");
setTimeout(() => {
console.log("第二");
}, 0);
console.log("第三");
這將輸出:
第一
第三
第二
在 JavaScript 中,有幾種方法來處理非同步操作:
程式碼範例:
console.log("開始");
function asyncTask(callback) {
setTimeout(() => {
console.log("非同步任務完成");
callback();
}, 2000);
}
asyncTask(() => {
console.log("任務完成");
});
console.log("結束");
程式碼範例:
console.log("開始");
const asyncTask = new Promise((resolve) => {
setTimeout(() => {
console.log("非同步任務完成");
resolve();
}, 2000);
});
asyncTask.then(() => {
console.log("任務完成");
});
console.log("結束");
程式碼範例:
console.log("開始");
async function asyncTask() {
await new Promise((resolve) => {
setTimeout(() => {
console.log("非同步任務完成");
resolve();
}, 2000);
});
console.log("任務完成");
}
asyncTask();
console.log("結束");
為了更好地理解 JavaScript 的這些執行方式及其之間的區別,以下是多個方面的詳細比較:
方面 | 同步程式碼 | 非同步程式碼 |
---|---|---|
執行順序 | 按行序列執行 | 允許任務在背景運行,而其他程式碼繼續執行 |
性能 | 若涉及長時間任務,可能導致性能問題 | 在 I/O 密集型操作中性能更佳;防止瀏覽器環境中的 UI 凍結 |
程式碼複雜度 | 通常較簡單且易讀 | 可能更複雜,特別是在嵌套回呼函數的情況下(回呼地獄) |
記憶體使用 | 若等待長時間操作可能使用更多記憶體 | 對於長時間任務通常更有效率 |
可擴展性 | 對於有許多並發操作的應用程序擴展性較差 | 對於處理多個同時操作的應用程序更具可擴展性 |
這個比較突顯了同步和非同步程式碼之間的關鍵區別,幫助開發者根據其特定用例和性能需求選擇合適的方法。
在 JavaScript 中,微任務和巨任務是兩種排隊並在事件循環的不同部分執行的任務,這決定了 JavaScript 如何處理非同步操作。
微任務和巨任務都在事件循環中排隊和執行,但它們具有不同的優先順序和執行上下文。微任務在巨任務隊列中的下一個任務之前不斷處理,直到微任務隊列為空。而巨任務則在微任務隊列清空後執行,並在下一個事件循環周期開始之前執行。
微任務是指需要在當前操作完成後但在下一個事件循環周期開始之前執行的任務。微任務優先於巨任務,並在微任務隊列為空之前不斷處理。
.then()
或 .catch()
處理器時)process.nextTick()
console.log("開始");
Promise.resolve().then(() => {
console.log("微任務");
});
console.log("結束");
開始
結束
微任務
巨任務是在微任務隊列清空後執行的任務,並在下一個事件循環周期開始之前執行。這些任務代表如 I/O 或渲染等操作,通常在特定事件後或延遲後安排。
console.log("開始");
setTimeout(() => {
console.log("巨任務");
}, 0);
console.log("結束");
開始
結束
巨任務
方面 | 微任務 | 巨任務 |
---|---|---|
執行時間 | 在當前腳本之後立即執行,並在渲染之前 | 在下一個事件循環迭代中執行 |
隊列優先順序 | 優先級較高,在巨任務之前處理 | 優先級較低,在所有微任務完成後處理 |
範例 | Promise、queueMicrotask()、MutationObserver | setTimeout()、setInterval()、I/O 操作、UI 渲染 |
用例 | 需要盡快執行而不讓事件循環讓步的任務 | 可以延遲或不需要立即執行的任務 |
事件循環是 JavaScript 中一個基本概念,能夠在 JavaScript 雖然是單執行緒的情況下啟用非阻塞的非同步操作。它負責處理非同步回呼並確保 JavaScript 在不被耗時操作阻塞的情況下平穩運作。
事件循環是一種機制,它使 JavaScript 能夠有效地處理非同步操作。它不斷檢查呼叫堆疊和任務隊列(或微任務隊列),以確定下一個應該執行的函數。
若要更好地理解事件循環,了解 JavaScript 的內部運作是很重要的。需注意的是 JavaScript 是一種 單執行緒 語言,這意味著它一次只能做一件事情。只有一個呼叫堆疊,這個堆疊存儲待執行的函數。這使同步程式碼簡單,但對於像從伺服器獲取數據或設置計時器這樣的任務,會耗時間完成。沒有事件循環的話,JavaScript 將會一直等待這些任務,什麼都不會發生。
呼叫堆疊是當前正在執行的函數所在的位置。JavaScript 在處理程式碼時,不斷地將函數添加到堆疊,也不斷地將其從堆疊中刪除。
當遇到像 setTimeout、fetch 或 Promise 之類的非同步任務時,JavaScript 將該任務委派給瀏覽器的 Web API(如計時器 API、網路 API 等),這些 API 在背景中處理該任務。
一旦非同步任務完成(例如,計時器完成,或從伺服器接收到數據),回呼(處理結果的函數)會移動到任務隊列(或在 Promise 的情況下進入微任務隊列)。
JavaScript 繼續執行同步程式碼。一旦呼叫堆疊空了,事件循環會從任務隊列(或微任務隊列)中取出第一個任務並將其放到呼叫堆疊中執行。
這個過程重複進行。事件循環確保在當前同步任務完成後,所有的非同步任務都被處理。
現在我們對事件循環的運作有了更好更清晰的理解,讓我們通過一些範例來鞏固我們的理解。
function exampleOne() {
console.log("開始");
setTimeout(() => {
console.log("計時結束");
}, 1000);
Promise.resolve().then(() => {
console.log("已解決");
});
console.log("結束");
}
exampleOne();
開始
結束
已解決
計時結束
function exampleTwo() {
console.log("開始");
setTimeout(() => {
console.log("計時器 1");
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1 已解決");
setTimeout(() => {
console.log("計時器 2");
}, 0);
return Promise.resolve().then(() => {
console.log("Promise 2 已解決");
});
});
console.log("結束");
}
exampleTwo();
開始
結束
Promise 1 已解決
Promise 2 已解決
計時器 1
計時器 2
function exampleThree() {
console.log("步驟 1:同步");
setTimeout(() => {
console.log("步驟 2:計時器 1");
}, 0);
Promise.resolve().then(() => {
console.log("步驟 3:Promise 1 已解決");
Promise.resolve().then(() => {
console.log("步驟 4:Promise 2 已解決");
});
setTimeout(() => {
console.log("步驟 5:計時器 2");
}, 0);
});
setTimeout(() => {
console.log(
"步驟 6:立即(使用 setTimeout 設置 0 延遲作為後備)"
);
}, 0);
console.log("步驟 7:同步結束");
}
exampleThree();
步驟 1:同步
步驟 7:同步結束
步驟 3:Promise 1 已解決
步驟 4:Promise 2 已解決
步驟 2:計時器 1
步驟 6:立即(使用 setTimeout 設置 0 延遲作為後備)
步驟 5:計時器 2
在 JavaScript 中,掌握同步和非同步操作,以及理解事件循環和它如何處理任務,是編寫高效和性能良好應用程序的關鍵。
提供的範例逐步說明了同步程式碼、 promises、計時器和事件循環之間的交互。理解這些概念是掌握 JavaScript 中非同步程式設計的關鍵,確保你的程式碼有效運行,避免常見的陷阱,如競賽條件或意外的執行順序。
為了確保你不會錯過本系列的任何部分,並與我聯繫,進行更深入的軟體開發(Web、伺服器、手機或抓取/自動化)討論、推播通知和其他令人興奮的技術主題,請關注我:
敬請期待並快樂編碼 👨💻🚀