瀏覽器EventLoop執行過程解析

黑客與碼農發表於2018-08-08

EventLoop又叫事件迴圈,用來控制瀏覽器中事件的執行過程。

瀏覽器JS執行過程

如下圖所示,瀏覽器存在執行棧(單執行緒執行JS程式碼)、任務佇列及一些WEB API。

EventLoop

在瀏覽器中執行如下程式碼:

console.log(1);

setTimeout(function(){
    console.log(2);
}, 5000);

console.log(3);

// 輸出:
// 1
// 3
// 2
複製程式碼

執行過程如下(演示地址):

  1. 執行console.log,輸出1
  2. 執行setTimeout,啟動定時器
  3. 執行console.log,輸出3
  4. 定時器到達時間,把回撥放入任務佇列
  5. 執行棧是空的,從任務佇列取任務,放入執行棧
  6. 執行回撥中的console.log,輸出2

理解了這個執行過程,就能明白為什麼下面程式碼(setTimeout時間設為0)的輸出結果與上面相同:

console.log(1);

setTimeout(function(){
    console.log(2);
}, 0);

console.log(3);

// 輸出:
// 1
// 3
// 2
複製程式碼

執行過程:

  1. 執行console.log,輸出1
  2. 執行setTimeout,啟動定時器
  3. 定時器到達時間,把回撥放入任務佇列
  4. 執行棧非空,繼續執行console.log,輸出3
  5. 執行棧為空,從任務佇列取任務,放入執行棧
  6. 執行回撥中的console.log,輸出2

巨集任務/微任務

並不是所有非同步任務的執行優先順序都相同,微任務(microtask)比巨集任務(macrotask)要優先執行。

在瀏覽器環境中,常見的巨集任務有setTimeoutMessageChannelpostMessagesetImmediate;常見的微任務有MutationObseverPromise.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

參考資料:

淺談js執行機制(執行緒)

理解 JavaScript 中的 macrotask 和 microtask

相關文章