EventLoop又叫事件迴圈,用來控制瀏覽器中事件的執行過程。
瀏覽器JS執行過程
如下圖所示,瀏覽器存在執行棧(單執行緒執行JS程式碼)、任務佇列及一些WEB API。
在瀏覽器中執行如下程式碼:
console.log(1);
setTimeout(function(){
console.log(2);
}, 5000);
console.log(3);
// 輸出:
// 1
// 3
// 2
複製程式碼
執行過程如下(演示地址):
- 執行
console.log
,輸出1 - 執行
setTimeout
,啟動定時器 - 執行
console.log
,輸出3 - 定時器到達時間,把回撥放入任務佇列
- 執行棧是空的,從任務佇列取任務,放入執行棧
- 執行回撥中的
console.log
,輸出2
理解了這個執行過程,就能明白為什麼下面程式碼(setTimeout時間設為0)的輸出結果與上面相同:
console.log(1);
setTimeout(function(){
console.log(2);
}, 0);
console.log(3);
// 輸出:
// 1
// 3
// 2
複製程式碼
執行過程:
- 執行
console.log
,輸出1 - 執行
setTimeout
,啟動定時器 - 定時器到達時間,把回撥放入任務佇列
- 執行棧非空,繼續執行
console.log
,輸出3 - 執行棧為空,從任務佇列取任務,放入執行棧
- 執行回撥中的
console.log
,輸出2
巨集任務/微任務
並不是所有非同步任務的執行優先順序都相同,微任務(microtask)比巨集任務(macrotask)要優先執行。
在瀏覽器環境中,常見的巨集任務有setTimeout
、MessageChannel
、postMessage
、setImmediate
;常見的微任務有MutationObsever
和Promise.then
。
從下面這段程式碼來看瀏覽器的執行過程:
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise3')
});
}, 0);
// 輸出:
// timeout1
// promise1
// promise2
// timeout2
// promise3
複製程式碼
以下為詳細執行過程:
- 開始
- 執行棧:setTimeout,setTimeout
- 微任務佇列:
- 巨集任務佇列:
- 輸出:
- 執行第一個
setTimeout
,啟動定時器1
- 執行棧:setTimeout
- 微任務佇列:
- 巨集任務佇列:
- 輸出:
定時器1
時間到,將回撥T1
放入巨集任務佇列- 執行棧:setTimeout
- 微任務佇列:
- 巨集任務佇列:回撥T1
- 輸出:
- 執行
setTimeout
,啟動定時器2
- 執行棧:
- 微任務佇列:
- 巨集任務佇列:回撥T1
- 輸出:
定時器2
時間到,將回撥T2
放入巨集任務佇列- 執行棧:
- 微任務佇列:
- 巨集任務佇列:回撥T1,回撥T2
- 輸出:
- 執行棧為空,微任務佇列為空,從巨集任務佇列中取出
回撥T1
,放入執行棧- 執行棧:console.log,Promise.then,Promise.then
- 微任務佇列:
- 巨集任務佇列:回撥T2
- 輸出:
- 執行
console.log
,輸出timeout1- 執行棧:Promise.then,Promise.then
- 微任務佇列:
- 巨集任務佇列:回撥T2
- 輸出:timeout1
- 執行
Promise.then
,將回撥P1
放入微任務佇列- 執行棧:Promise.then
- 微任務佇列:回撥P1
- 巨集任務佇列:回撥T2
- 輸出:timeout1
- 執行下一個
Promise.then
,將回撥P2
放入微任務佇列- 執行棧:
- 微任務佇列:回撥P1,回撥P2
- 巨集任務佇列:回撥T2
- 輸出:timeout1
- 執行棧為空,從微任務佇列取出
回撥P1
,放入執行棧- 執行棧:console.log
- 微任務佇列:回撥P2
- 巨集任務佇列:回撥T2
- 輸出:timeout1
- 執行
console.log
,輸出promise1- 執行棧:
- 微任務佇列:回撥P2
- 巨集任務佇列:回撥T2
- 輸出:timeout1,promise1
- 執行棧為空,從微任務佇列取出
回撥P2
,放入執行棧- 執行棧:console.log
- 微任務佇列:
- 巨集任務佇列:回撥T2
- 輸出:timeout1,promise1
- 執行
console.log
,輸出promise2- 執行棧:
- 微任務佇列:
- 巨集任務佇列:回撥T2
- 輸出:timeout1,promise1,promise2
- 執行棧為空,微任務佇列為空,從巨集任務佇列取出
回撥T2
,放入執行棧- 執行棧:console.log,Promise.then
- 微任務佇列:
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2
- 執行
console.log
,輸出timeout2- 執行棧:Promise.then
- 微任務佇列:
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2,timeout2
- 執行
Promise.then
,將回撥P3
放入微任務佇列- 執行棧:
- 微任務佇列:回撥P3
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2,timeout2
- 任務佇列為空,從微任務佇列取出
回撥P3
,放入執行棧- 執行棧:console.log
- 微任務佇列:
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2,timeout2
- 執行
console.log
,輸出promise3- 執行棧:
- 微任務佇列:
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2,timeout2,promise3
- 完成
- 執行棧:
- 微任務佇列:
- 巨集任務佇列:
- 輸出:timeout1,promise1,promise2,timeout2,promise3
參考資料: