第 41 題:請描述一下 Javascript 事件迴圈機制?

語言: CN / TW / HK

事件迴圈機制

在事件迴圈中,每進行一次迴圈操作稱為 tick,每一次 tick 的任務處理是比較複雜的,但關鍵步驟如下:

  1. 執行一個巨集任務(棧中沒有就從事件佇列中獲取)

  2. 執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中

  3. 巨集任務執行完畢後,立即執行當前微任務佇列中的所有微任務(依次執行)

  4. 當前巨集任務執行完畢,開始檢查渲染,然後 GUI 執行緒接管渲染

  5. 渲染完畢後,JS 執行緒繼續接管,開始下一個巨集任務(從事件佇列中獲取)

流程圖如下:

<img src="https://noxussj.top:3000/41/1.png"></img>

那麼什麼是巨集任務和微任務呢?

巨集任務

(macro)task(又稱之為巨集任務),可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行)

瀏覽器為了能夠使得 JS 內部(macro)task 與 DOM 任務能夠有序的執行,會在一個(macro)task 執行結束後,在下一個(macro)task 執行開始前,對頁面進行重新渲染

(macro)task 主要包含:script(整體程式碼)、setTimeout、setInterval

微任務

microtask(又稱為微任務),可以理解是在當前(macro) task 執行結束後立即執行的任務。也就是說,在當前(macro)task 任務後,下一個(macro)task 之前,在渲染之前。

所以它的響應速度相比 setTimeout(setTimeout 是(macro)task)會更快,因為無需等渲染。也就是說,在某一個 macrotask 執行完後,就會將在它執行期間產生的所有 microtask 都執行完畢(在渲染前)

microtask 主要包含:Promise.then、await 方法後面的程式碼屬於.then(await 相當於一個 Promise)

栗子

async function async1() {
    console.log('A');
    await async2();
    console.log('B');
}
async function async2() {
    console.log('C');
}
console.log('D');
setTimeout(function() {
    console.log('E');
});
async1();
new Promise(function(resolve) {
    console.log('F');
    resolve();
}).then(function() {
    console.log('G');
});
console.log('H');

首先我們需要明白以下幾件事情

任務佇列主要包括以下 3 個,巨集任務佇列、微任務佇列、執行棧

  1. 一開始執行棧,以及微任務佇列為空,巨集任務只有一個 script 程式碼塊

  2. 執行棧為空時,就把下一個巨集任務新增到執行棧中執行

  3. 開始執行巨集任務 script

  4. 程式往下執行遇到了 console.log('D'),這個時候直接列印 結果為: // D

  5. 然後繼續往下執行遇到了 setTimeout,它屬於巨集任務所以先把它新增到巨集任務佇列中 任務佇列狀態如下 執行棧:script 巨集任務佇列:setTimeout 微任務佇列:空

  6. 繼續往下執行遇到了 async1()方法,執行該方法遇到了 console.log('A'),直接列印 結果為:// D A

  7. 繼續往下執行遇到了 async2()方法,執行該方法遇到了 console.log('C'),直接列印 結果為:// D A C

  8. async2()方法內的程式都執行完畢,回到上一層 async1()中,遇到 console.log('B'),它在 await async2() 的後面,所以屬於非同步並且新增到微任務佇列中,然後回到最外面一層 任務佇列狀態如下 執行棧:script 巨集任務佇列:setTimeout 微任務佇列:console.log('B')

  9. 繼續往下執行遇到了 new Promise(),該作用域內同步任務。執行作用域內方法,遇到了 console.log('F'),直接列印 結果為:// D A C F

  10. 繼續往下執行遇到了.then 屬於非同步,將 then 內部的程式碼新增到微任務佇列中 任務佇列狀態如下 執行棧:script 巨集任務佇列:setTimeout 微任務佇列:console.log('B')、console.log('G')

  11. 該 new Promise 方法執行完畢,回到最後外面,遇到了 console.log('H'),直接列印 結果為:// D A C F H

  12. 當前 script 程式碼塊程式執行完畢,也就是當前巨集任務執行完畢。在執行該巨集任務的過程中,如果某個微任務已經準備就緒好了會標記一個準備就緒的狀態

  13. 將已就緒的微任務從微任務佇列中新增到執行棧中 任務佇列狀態如下 執行棧:console.log('B')、console.log('G') 巨集任務佇列:setTimeout 微任務佇列:空

  14. 開始執行執行棧的任務,按順序執行直接列印 結果為:// D A C F H B G 任務佇列狀態如下 執行棧:空 巨集任務佇列:setTimeout 微任務佇列:空

  15. 當前的執行棧為空,則把巨集任務佇列中的 setTimeout 新增到執行棧中執行

  16. setTimeout 中遇到了 console.log('E')直接列印 結果為:// D A C F H B G E

  17. 當前執行棧已執行完畢,檢測是否有微任務(沒有),檢測是否有巨集任務(沒有)。整個程式執行完畢

此題留下了一個問題,假設遇到多個 setTimeout 延遲執行的時間不同時,該如何執行?

參考資料 從一道題淺說 JavaScript 的事件迴圈

文章的內容/靈感都從下方內容中借鑑