瀏覽器中的事件循環

瀏覽器中的事件循環

總所周知 JS 運行在瀏覽器中,以單線程方式運行,每一個window一個JS線程。那麼瀏覽器是如何處理js中的I/O讀取、用戶點擊、setTimeout等異步事件,並使其餘js代碼不被阻塞的呢?html

瀏覽器中的事件循環就是其解決方式。簡單來講瀏覽器中的事件循環的機制是將產生的異步事件產生的回調暫時存儲在事件隊列中,等到合適的時機再去執行隊列中的異步事件的回調。web

運行時概念

要了解瀏覽器中的事件循環,須要先弄明白兩個重要的運行時概念。segmentfault

執行棧,函數調用時造成一個調用幀,並壓入棧中,當函數返回時,則幀彈棧。api

隊列

任務隊列,每個任務都包含一個處理該任務的函數,當任務產生時,任務及其處理函數會被做爲一個總體推入任務隊列中(例如:一個setTimeout,到達時間時,setTimeOut及其回調函數會做爲一個任務被推入任務隊列中)。任務隊列按照先進先出的順序執行。當任務隊列裏的任務須要被處理的時候(即調用任務的處理函數時),將會被移出隊列,調用其處理函數,此時造成一個調用幀,並壓入執行棧。promise

此時執行棧中的調用幀,直到執行棧爲空,而後再去處理隊列中的另外一個任務。瀏覽器

瀏覽器任務

瀏覽器中的任務分爲兩種:task(macroTask 宏任務)和microtask(微任務)。不一樣的任務按照不一樣的規則執行。併發

task

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

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

microTask

一個事件循環中包含一個microTask queue。 microTask queue包含:webapp

  • 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;次步驟和三、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

相關文章
相關標籤/搜索