第5題- 深入理解事件循環機制

面試題目(頭條筆試):

直接上題,答對解釋通算你贏,就不用看解析了。

點擊頁面後,下面代碼的輸出結果是什麼?

document.addEventListener('click', function(){
	Promise.resolve().then(()=> console.log(1));
	console.log(2);
})

document.addEventListener('click', function(){
	Promise.resolve().then(()=> console.log(3));
	console.log(4);
})

輸出結果

2, 1, 4, 3

答案解析:

JS異步執行原理: js執行引擎只有一個主線程執行代碼邏輯,遇到需要異步執行的任務代碼,會將其添加事件隊列中。當主線程空閒時,輪詢事件隊列中可以執行的任務,將其放到主線程進行執行,以此類推,直到事件隊列中無可執行的任務。如下圖所示:

image

JS引擎只是執行事件隊列中的異步代碼,但事件隊列中的信息來源並不是JS引擎,而是由瀏覽器中的其他相關線程產生的,如下圖所示:

image

以 http 傳輸線程爲例:
最常見的就是 js 代碼發出 ajax 請求,然後就是交給瀏覽器的http線程去處理了,當後端有數據返回時,http 線程在事件隊列中生成一個數據已ready好的事件,等待 JS 主線程空閒時執行。

再比如,我們常見的click,mouse事件,都是GUI 事件觸發線程生成的。當用戶點擊頁面時,GUI 事件觸發線程就會在事件隊列中生成一個click事件,等待 JS 主線程空閒時執行。

宏任務 & 微任務

瀏覽器中的事件循環的任務隊列被劃分爲宏任務和微任務兩種類型:

macrotask:包含執行整體的js代碼script,事件回調,XHR回調,定時器(setTimeoutsetIntervalsetImmediate),IO操作,UI render

microtask:更新應用程序狀態的任務,包括promise回調,MutationObserverprocess.nextTickObject.observe

mactotask & microtask的執行順序如下圖所示:

image

總結起來,一次事件循環的步驟包括:

  1. 檢查macrotask隊列是否爲空,非空則到2,爲空則到3
  2. 執行macrotask中的一個任務
  3. 繼續檢查microtask隊列是否爲空,若有則到4,否則到5
  4. 執行當前microtask隊列中的所有任務,直至清空爲止,執行完成返回到步驟3
  5. 執行視圖更新

視圖渲染的時機

回顧上面的事件循環示意圖,update rendering(視圖渲染)發生在本輪事件循環的microtask隊列被執行完之後,也就是說執行任務的耗時會影響視圖渲染的時機。通常瀏覽器以每秒60幀(60fps)的速率刷新頁面,據說這個幀率最適合人眼交互,大概16.7ms渲染一幀,所以如果要讓用戶覺得順暢,單個macrotask及它相關的所有microtask最好能在16.7ms內完成。

但也不是每輪事件循環都會執行視圖更新,瀏覽器有自己的優化策略,例如把幾次的視圖更新累積到一起重繪,重繪之前會通知requestAnimationFrame執行回調函數,也就是說requestAnimationFrame回調的執行時機是在一次或多次事件循環的UI render階段。

示例如下:

setTimeout(function() {console.log('timer1')}, 0)

requestAnimationFrame(function(){
	console.log('UI update')
})

setTimeout(function() {console.log('timer2')}, 0)

new Promise(function executor(resolve) {
	console.log('promise 1')
	resolve()
	console.log('promise 2')
}).then(function() {
	console.log('promise then')
})

console.log('end')

可能輸出結果:

promise 1, promise 2, end, promise then, timer1, timer2, UI update

promise 1, promise 2, end, promise then, UI update, timer1, timer2

總結:

  1. 事件循環是js實現異步的核心
  2. 每輪事件循環分爲3個步驟:
    1. 執行macrotask隊列的一個任務
    2. 執行完當前microtask隊列的所有任務
    3. UI render
  3. 瀏覽器只保證requestAnimationFrame的回調在重繪之前執行,沒有確定的時間,何時重繪由瀏覽器決定

補充:Node 與瀏覽器的 Event Loop 差異:

瀏覽器環境下,microtask 的任務隊列是每個 macrotask 執行完之後執行。而在 Node.js 中,microtask 會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask 隊列的任務。

在這裏插入圖片描述
接下我們通過一個例子來說明兩者區別:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

瀏覽器端運行結果:timer1=>promise1=>timer2=>promise2

Node 端運行結果:timer1=>timer2=>promise1=>promise2

瀏覽器和 Node 環境下,microtask 任務隊列的執行時機不同

  1. Node 端,microtask 在事件循環的各個階段之間執行

  2. 瀏覽器端,microtask 在事件循環的 macrotask 執行完之後執行

具體可參考:https://blog.csdn.net/Fundebug/article/details/86487117


掃一掃 關注我的公衆號【前端名獅】,更多精彩內容陪伴你!
【前端名獅】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章