瀏覽器中 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')
結果:
分析:
以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 Explained