面試題目(頭條筆試):
直接上題,答對解釋通算你贏,就不用看解析了。
點擊頁面後,下面代碼的輸出結果是什麼?
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執行引擎只有一個主線程執行代碼邏輯,遇到需要異步執行的任務代碼,會將其添加事件隊列中。當主線程空閒時,輪詢事件隊列中可以執行的任務,將其放到主線程進行執行,以此類推,直到事件隊列中無可執行的任務。如下圖所示:
JS引擎只是執行事件隊列中的異步代碼,但事件隊列中的信息來源並不是JS引擎,而是由瀏覽器中的其他相關線程產生的,如下圖所示:
以 http 傳輸線程爲例:
最常見的就是 js 代碼發出 ajax 請求,然後就是交給瀏覽器的http線程去處理了,當後端有數據返回時,http 線程在事件隊列中生成一個數據已ready好的事件,等待 JS 主線程空閒時執行。
再比如,我們常見的click,mouse事件,都是GUI 事件觸發線程生成的。當用戶點擊頁面時,GUI 事件觸發線程就會在事件隊列中生成一個click事件,等待 JS 主線程空閒時執行。
宏任務 & 微任務
瀏覽器中的事件循環的任務隊列被劃分爲宏任務和微任務兩種類型:
macrotask:包含執行整體的js代碼
script
,事件回調,XHR回調,定時器(setTimeout
、setInterval
、setImmediate
),IO操作,UI render
microtask:更新應用程序狀態的任務,包括promise回調,
MutationObserver
,process.nextTick
,Object.observe
mactotask & microtask的執行順序如下圖所示:
總結起來,一次事件循環的步驟包括:
- 檢查macrotask隊列是否爲空,非空則到2,爲空則到3
- 執行macrotask中的一個任務
- 繼續檢查microtask隊列是否爲空,若有則到4,否則到5
- 執行當前microtask隊列中的所有任務,直至清空爲止,執行完成返回到步驟3
- 執行視圖更新
視圖渲染的時機
回顧上面的事件循環示意圖,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
總結:
- 事件循環是js實現異步的核心
- 每輪事件循環分爲3個步驟:
- 執行macrotask隊列的一個任務
- 執行完當前microtask隊列的所有任務
- UI render
- 瀏覽器只保證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 任務隊列的執行時機不同
-
Node 端,microtask 在事件循環的各個階段之間執行
-
瀏覽器端,microtask 在事件循環的 macrotask 執行完之後執行
具體可參考:https://blog.csdn.net/Fundebug/article/details/86487117
掃一掃 關注我的公衆號【前端名獅】,更多精彩內容陪伴你!