JS 中的 Event Loop 是什么?
?大家好,我是前端西瓜哥,今天來認識一下什么是 Event Loop。
Event Loop,簡單翻譯就是 事件循環,是 JS 語言下實現運行時的一個機制。
JS 的異步并不像其他語言(比如 Java)的異步那樣可以實現真正的并發執行,本身其實是個單線程。
JS 是維護了一個 任務隊列,每當要執行一些異步任務,比如定時器或者是點擊按鈕觸發的事件響應函數。它們不會立即執行,而是放到這個隊列里,等待已經在排隊的其他任務先執行完,才輪到它們。
隊列是一個操作受限的有序列表,表現為為先進入的元素必須先出去,即 “先進先出”,很像排隊的感覺。
不過也有一些特殊的隊列,比如優先級隊列,它是優先級高的元素先出隊。
之所以叫 Event Loop,因為它的邏輯可以描述為下面代碼:?
while (queue.waitForMessage()) {
queue.processNextMessage();
}
當一個任務被完成后,隊列會變成等待下一個任務狀態,然后處理下一個任務,如此循環往復。
因為 JS 的代碼執行本身是一個單線程,為了不讓執行阻塞,JS 會把網絡請求操作、渲染瀏覽器頁面等操作,交給其他的線程,等待其他線程處理好把結果返回給 JS。
所以 JS 不合適 CPU 密集型,更適合 IO 密集型的場景。因為它只有一個線程,如果計算耗時太長,就會阻塞其他要執行的任務,導致卡頓,甚至崩潰。
setTimeout 定時器并不準
setTimeout 在特定時間后要執行的函數,并不會立即執行,而是會先放到任務隊列中,先等待前面的任務同步執行完成了,才能執行我們這個。
下面看一個例子,因為第一個 setTimout 有一個非常耗時的同步任務,導致下一個 setTimeout 的執行阻塞,比前面一個 setTimeout 執行要慢半秒。
const start = new Date().getTime();
setTimeout(() => {
console.log('1:', new Date().getTime() - start);
let num = 0;
for (let i = 0; i < 999999999; i++) {
num = i;
}
}, 1000);
setTimeout(() => {
console.log('2:', new Date().getTime() - start);
}, 1000);
/**
* 輸出結果:
* 1: 1001
* 2: 1505
*/
定時器的時間,指的是能執行的最早時間,但不能保證一定能在這個時間點立即執行。
宏任務和微任務
任務隊列并不是嚴格意義上的先進先出的正常隊列,是可以調整執行順序的。
我們將要執行的任務分為宏任務和微任務,其中宏任務是正常的先進先出,而微任務則是可以插隊,優先于宏任務先執行。宏任務必須在所有微任務執行后才能執行。
當我們給任務隊列添加一個微任務時,它會跑到任務隊列宏任務前。多個微任務入隊時,會保持它們的相對順序。
宏任務有:
- script,即 HTML 嵌入的腳本。
- setTimeout / setInterval 定時器。
- setImmediate,這是 nodejs 特有的 API。
- requestAnimationFrame,會在頁面重繪前執行。
- I/O 操作,比如網絡請求完成的回調函數執行任務、還比如點擊按鈕要執行的回調等。這些操作其實是其他的線程完成后觸發的,暫且歸納為 I/O 操作。
微任務有:
- Promise 從 pending 狀態轉換為其他狀態時,觸發 then/catch/finaly 中的函數,比如 Promise.resolve().then(fn)。這是最常見的微任務。
- MutationObserver,用于監聽 DOM 的變化。
- process.nextTick,nodejs 特有的 API。
任務隊列,理論上一個就夠了,但也可以是多個隊列的組合,沒有強行要求。
多個任務隊列的實現可以更好地實現優先級的控制,比如對于定時器任務,理論上應該是在多個宏任務中最先執行比較好。瀏覽器沒考慮這種情況,但 nodejs 給宏任務中也設置優先級,會讓定時器任務最先執行。
Event Loop 還是挺復雜的,標準文檔也比較長,我也沒怎么看,感興趣可以看看。
https://html.spec.whatwg.org/multipage/webappapis.html#event-loops。
一道經典異步題
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
解題思路為:
- 找到同步代碼。同步代碼有:普通同步代碼、new Promise(fn) 執行傳入的回調函數、async 執行時遇到 await 的前面部分(包括 await 的右側函數執行也是同步的,這里是易錯點)。
- 看看任務隊列中有哪些微任務和宏任務,記住微任務全執行完了才會執行宏任務。
- 執行任務,任務里面的異步任務又按順序進入到任務隊列。
結果是:
// 同步代碼
script start
async1 start
async2
promise1
script end
// 微任務
async1 end
promise2
// 宏任務
setTimeout
結尾
JS 運行機制是單線程,當有多個異步任務要同時執行,并不能并發執行,必須讓優先級高的任務執行完才能執行后面的。如果正在執行的任務比較耗時,會導致后面的任務被阻塞。
Event Loop 的機制中,最基本的一條就是:微任務比宏任務先執行。