瀏覽器中 JS 的事件循環機制

目錄

  • 事件循環機制
  • 宏任務與微任務
  • 實例分析
  • 參考

1.事件循環機制

瀏覽器事件循環機制

瀏覽器執行JS程式碼大致可以分為三個步驟,而這三個步驟的往複構成了JS的事件循環機制(如圖)。

第一步:主執行緒(JS引擎執行緒)中執行JS整體程式碼或回調函數(也就是宏任務),執行過程中會將對象存儲到堆(heap)中,將基礎類型和函數加入到棧(stack)中,執行完畢後會釋放堆或退出棧。執行完這個宏任務後,會判斷微任務棧(microtask queue)是否為空,如果不為空,則會將所有的微任務依次取出並執行。如果在這個過程中觸發了任何 Web APIs 將進行第二步操作。

第二步:調用 Web API,並在合適的時候將回調函數加入到事件回調棧(event queue)中。比如執行了setTimeout(callback1, 1000),會創建一個計時器,並且在另一個執行緒(瀏覽器定時觸發執行緒)裡面監聽計時器是否過期,等到計時器過期後,會將對應回調 callback1加入事件回調棧中。

第三步:等到第一步中的微任務執行完畢之後,會判斷事件回調棧(event queue)是否為空。如果不為空,則會取出並執行最先進入隊列的回調函數,執行過程如同第一步。如果為空,則會視情況進行等待或掛起主執行緒。

補充說明:瀏覽器的內核是多執行緒的,常駐執行緒有瀏覽器 GUI 渲染執行緒、JavaScript 引擎執行緒、瀏覽器定時觸發器執行緒、瀏覽器事件觸發執行緒、瀏覽器 http 非同步請求執行緒。

2.宏任務與微任務

宏任務(macrotask):script(整體程式碼)、setTimeout/setInterval、I/O、UI rendering等

微任務(microtask):Promise、MutationObserver等

JS程式碼執行過程——宏任務與微任務的執行示意圖:

宏任務與微任務的執行示意圖

如圖,可以看出JS執行過程中,是先執行一個宏任務,再執行這個宏任務產生的對應微任務,執行完畢後,再執行後面的宏任務,以此往複。

3.實例分析

使用瀏覽器:Chrome Version 80.0.3987.163

第一組:

比較 setTimeout 與 Promise

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
}, 0);

Promise.resolve().then(() => {
  console.log('microtask: promise')
})

console.log('end')

結果:

比較 setTimeout 與 Promise

分析:

以JS的事件循環機制來分析。首先,script(整體程式碼)算是一個宏任務,執行完畢,會先後輸出”start”和”end”,然後執行這個過程中產生的微任務,即promis.then中的回調,輸出”microtask: promise”;這個過程中也調用了 Web API 中的 setTimeout,會創建一個計時器,過期後將回調添加到事件回調棧中;然後再執行回調(第二個宏任務),輸出”setTimeout”。與瀏覽器運行輸出一致,符合預期。

第二組:

宏任務與微任務的執行順序對比

function func1() {
  console.log('func1')
  Promise.resolve().then(() => {
    console.log('microtask.promise1')
  })
}

function func2() {
  console.log('func2')
  Promise.resolve().then(() => {
    console.log('microtask.promise2')
  })
}

function main() {
  func1()
  func2()
  setTimeout(func1, 0);
  setTimeout(func2, 0);
}

main()

結果:

宏任務與微任務的執行順序對比

分析:

從輸出結果可以看出,當一個宏任務執行完畢後,會接著執行相應的所有微任務,執行完畢後,再執行後續的宏任務,並以往複,與預期相符。

4.參考

並發模型與事件循環

Javascript event loop

JavaScript Event Loop Explained

HTML系列:macrotask和microtask

【翻譯】Promises/A+規範