瀏覽器中的事件迴圈

叫我女王大人發表於2019-03-03

瀏覽器中的事件迴圈

總所周知 JS 執行在瀏覽器中,以單執行緒方式執行,每個window一個JS執行緒。那麼瀏覽器是如何處理js中的I/O讀取、使用者點選、setTimeout等非同步事件,並使其他js程式碼不被阻塞的呢?

瀏覽器中的事件迴圈就是其解決方式。簡單來說瀏覽器中的事件迴圈的機制是將產生的非同步事件產生的回撥暫時儲存在事件佇列中,等到合適的時機再去執行佇列中的非同步事件的回撥。

執行時概念

要了解瀏覽器中的事件迴圈,需要先弄明白兩個重要的執行時概念。

執行棧,函式呼叫時形成一個呼叫幀,並壓入棧中,當函式返回時,則幀彈棧。

佇列

任務佇列,每一個任務都包含一個處理該任務的函式,當任務產生時,任務及其處理函式會被作為一個整體推入任務佇列中(例如:一個setTimeout,到達時間時,setTimeOut及其回撥函式會作為一個任務被推入任務佇列中)。任務佇列按照先進先出的順序執行。當任務佇列裡的任務需要被處理的時候(即呼叫任務的處理函式時),將會被移出佇列,呼叫其處理函式,此時形成一個呼叫幀,並壓入執行棧。

此時執行棧中的呼叫幀,直到執行棧為空,然後再去處理佇列中的另一個任務。

瀏覽器任務

瀏覽器中的任務分為兩種:task(macroTask 巨集任務)和microtask(微任務)。不同的任務按照不同的規則執行。

task

一個事件迴圈裡有多個task queue,其中的包含多個任務,每個任務嚴格的按照先進先出的順序執行。在一個task執行結束後下一個task執行之前,瀏覽器可對頁面進行重新渲染。
task queue中包含:

  • script整體程式碼
  • 瀏覽器事件(滑鼠事件、鍵盤事件等)
  • 定時事件(setTimeout、setInterval、setImmediate)
  • I/O事件(資源讀取等)
  • UI渲染

microTask

一個事件迴圈中包含一個microTask queue。
microTask queue包含:

  • Promise
  • Object.observe、MutationObserver

事件迴圈

執行至完成

一個任務完整的執行後,其他任務才會被執行。
即:執行棧中的呼叫幀,直到執行棧為空,然後再去處理佇列中的另一個任務。

新增任務至佇列

在瀏覽器中,當事件發生並且該事件繫結了事件監聽時,該事件發生後的任務才會被新增至佇列。

例如:為一個DOM元素button繫結onclick一個處理事件,只有當button元素上的click事件發生時,該事件發生後的任務會被新增至佇列。

再例如: setTimeout 接受兩個引數:待加入佇列的任務和一個延遲。延遲代表了任務被新增至任務佇列的時間,只有經過了延遲的時間,該任務才會被加入佇列。新增至佇列以後是否被處理,取決於佇列裡是否有其他任務。因此延遲的時間表示最少延遲時間,而非確切的等待時間。

事件迴圈程式模型

事件迴圈程式模型 步驟如下:

  1. 選擇第一個進入到 task queue中的任務[task],如果task queue中沒有任務,則直接進入第6步;
  2. 將當前事件迴圈的任務設定為第一步選出的[task];
  3. 執行任務[task];
  4. 將當前事件迴圈任務設定為null;
  5. 在task queue中刪除執行完畢的[task]任務;
  6. microtask階段:進入microtask檢查點;
  7. 按照瀏覽器介面更新策略渲染介面;
  8. 返回第1步;
其中第6步,microtask階段步驟如下:
  1. 設定microtask檢查點標記為true;
  2. 重複檢查microtask queue是否為空,若為空直接進入第3步;若不為空:
  • 選擇microtask queue中的第一個[microtask];
  • 將當前事件迴圈的任務設定為第3步選出的[microtask];
  • 執行任務[microtask];
  • 將當前事件迴圈任務設定為null;
  • 在microtask queue中刪除執行完畢的[microtask]任務;
  • 回到第2步
  1. 清理index database 事務;
  2. 設定microtask檢查點的標記為false;
事件迴圈程式模型總結

在事件迴圈中,首先從task queue中選擇最先進入的task執行,每執行完一個task都會檢查microtask queue是否為空,若不為空則執行完microtsk queue中的所有任務。然後再選擇task queue中最先進入的task執行,以此迴圈。

總結上述步驟為流程圖:

未命名檔案

用程式碼解釋

程式碼栗子1:

console.log(`這是開始`);

setTimeout(function cb() {
	console.log(`這是來自第一個回撥的訊息`);
}, 100);

console.log(`這是一條訊息`);

setTimeout(function cb1() {
	console.log(`這是來自第二個回撥的訊息`);
}, 0);

Promise.resolve().then(function() {
	console.log(`promise1`);
}).then(function() {
	console.log(`promise2`);
});

console.log(`這是結束`);
複製程式碼
輸出結果為:
    這是開始
    這是一條訊息
    這是結束
    promise1
    promise2
    這是來自第二個回撥的訊息
    這是來自第一個回撥的訊息
複製程式碼
步驟解析:
  1. 初始狀態佇列的資訊為:
    • task queue:run script;
    • microtask queue:【空】;
  2. 從task queue中拿出run script執行;執行完成後從task queue中刪除run script任務;
    • 當前事件迴圈執行棧:run script;
    • task queue:setTimeout2 callback;
    • microtask queue:promise1 then;
輸出:
    這是開始
    這是一條訊息
    這是結束
複製程式碼
  1. 進入microtask檢查點;從microtask queue中拿出promise1 then執行;將promise2 then推入microtask queue;執行完成後從microtask queue中刪除promise1 then任務;

    • 當前事件迴圈執行棧:promise1 then;
    • task queue:setTimeout2 callback;
    • microtask queue:promise2 then;
輸出:
    這是開始
    這是一條訊息
    這是結束
    promise1
複製程式碼
  1. 檢視microtask queue中是否還有任務;有則從microtask queue中拿出promise2 then執行;執行完成後從microtask queue中刪除promise2 then任務;

    • 當前事件迴圈執行棧:promise2 then;
    • task queue:setTimeout2 callback;
    • microtask queue:【空】;
輸出:
    這是開始
    這是一條訊息
    這是結束
    promise1
    promise2
複製程式碼
  1. 到達100ms後,將setTimeout1 callback推入task queue;次步驟和3、4步無明確的前後關係,依據所設定的時間長短而定;setTimeout1 callback被推入task queue中以後不一定會立刻執行,因為task queue中可能存在其他任務尚未執行;因此setTimeout1 callback實際執行時間點>=100ms;
    • task queue:setTimeout2 callback、setTimeout1 callback;
    • microtask queue:【空】;
  2. microtask queue為空,繼續取出task queue中的任務setTimeout2 callback執行,執行完成後從task queue中刪除setTimeout2 callback任務;
    • 當前事件迴圈執行棧:setTimeout2 callback;
    • task queue:setTimeout1 callback;
    • microtask queue:【空】;
輸出:
    這是開始
    這是一條訊息
    這是結束
    promise1
    promise2
    這是來自第二個回撥的訊息
複製程式碼
  1. 檢查microtask檢查點,microtask queue為空,繼續取出task queue中的任務setTimeout1 callback執行,執行完成後從task queue中刪除setTimeout1 callback任務;

    • 當前事件迴圈任務:setTimeout1 callback;
    • task queue:【空】;
    • microtask queue:【空】;
輸出:
    這是開始
    這是一條訊息
    這是結束
    promise1
    promise2
    這是來自第二個回撥的訊息
    這是來自第一個回撥的訊息
複製程式碼

複雜的程式碼栗子2:

console.log(`script start`)
async function async1() {
    await async2();
    console.log(`async1 end`);
    setTimeout(function() {
    	console.log(`async1 setTimeout`)
    }, 0);
}
async function async2() {
    console.log(`async2 end`);
    setTimeout(function() {
    	console.log(`async2 setTimeout`)
    }, 0);
}
async1();

setTimeout(function() {
    Promise.resolve().then(function() {
    	console.log(`setTimeout promise`);
    })
	console.log(`setTimeout`);
}, 0);

new Promise(resolve => {
    console.log(`Promise`)
    resolve()
})
.then(function() {
	console.log(`promise1`)
})
.then(function() {
	console.log(`promise2`)
})

console.log(`script end`)
複製程式碼
輸出結果為:
    script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise
	async1 setTimeout
複製程式碼
步驟解析:

async函式是promise的一個語法糖,簡單理解為:await中的語句相當於在promise.resolve()中;await後面的語句相當於.then中的語句

  1. 初始狀態的佇列資訊
    • task queue:run script;
    • microtask queue:【空】;
  2. 【task 階段】執行run script;根據上述對await的解釋,async1中的await async2()直接執行,async1中的await 後面的語句相當於promise then去處理
    • 當前事件迴圈執行棧:run script;
輸出:
	script start
複製程式碼

— 2.1 執行到呼叫async1()語句,在async1中執行await async2,async2中的語句直接執行,async2 setTimeout callback被推入task queue中;async1中的await async2後面的語句相當於promise then被推入microtask queue中;
​ – task queue:async2 setTimeout callback;
​ – microtask queue:async1中的await async2後面的語句;

輸出:
	script start 
	async2 end
複製程式碼

— 2.2. 執行到setTimeout將setTimeout callback 推入 task queue中;
​ – task queue:async2 setTimeout callback、setTimeout callback;
​ – microtask queue:async1中的await async2後面的語句;
— 2.3. 執行到promise,執行resolve,將promise then1推入microtask queue;
​ – task queue:async2 setTimeout callback、setTimeout callback;
​ – microtask queue:async1中的await async2後面的語句、promise then1;

輸出:
	script start
	async2 end 
	Promise
複製程式碼

— 2.4. 執行輸出script end;

輸出:
	script start
	async2 end
	Promise
	script end
複製程式碼
  1. 【microtask 階段】執行async1中的await async2後面的語句,輸出內容,並將async1 setTimeout callback推入task queue中

    • 當前事件迴圈執行棧:async1中的await async2後面的語句;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:promise then1;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
複製程式碼
  1. 【microtask 階段】執行promise then1,輸出內容,並將promise then2推入microtask queue中
    • 當前事件迴圈執行棧:promise then1;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:promise then2;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
複製程式碼
  1. 【microtask 階段】執行promise then2,輸出內容,microtask queue為空
    • 當前事件迴圈執行棧:promise then2;
    • task queue:async2 setTimeout callback、setTimeout callback、async1 setTimeout callback;
    • microtask queue:【空】;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
複製程式碼
  1. 【task 階段】microtask queue為空,進入task階段,執行async2 setTimeout callback,輸出內容
    • 當前事件迴圈執行棧:async2 setTimeout callback;
    • task queue:setTimeout callback、async1 setTimeout callback;
    • microtask queue:【空】;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout 
複製程式碼
  1. 【task 階段】microtask queue為空,執行setTimeout callback,將setTimeout promise推入microtask queue,輸出內容
    • 當前事件迴圈執行棧:setTimeout callback;
    • task queue:async1 setTimeout callback;
    • microtask queue:setTimeout promise;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout 
複製程式碼
  1. 【microtask 階段】檢查microtask queue,執行setTimeout promise,輸出內容
    • 當前事件迴圈執行棧:setTimeout promise;
    • task queue:async1 setTimeout callback;
    • microtask queue:【空】;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise 
複製程式碼
  1. 【task 階段】microtask queue為空,執行async1 setTimeout callback,輸出內容
    • 當前事件迴圈執行棧:async1 setTimeout callback;
    • task queue:【空】;
    • microtask queue:【空】;
輸出:
	script start
	async2 end
	Promise
	script end
	async1 end
	promise1
	promise2
	async2 setTimeout
	setTimeout
	setTimeout promise
	async1 setTimeout
複製程式碼

參考:

什麼是瀏覽器的事件迴圈(Event Loop)?

HTML標準-事件迴圈

MDN-併發模型與事件迴圈

一次弄懂Event Loop

相關文章