event loop整理

宏任務和微任務

讓我們從瀏覽器加載 script 說起,當瀏覽器加載完 script 之後,不考慮 script 標籤的 defer 屬性,script 將被立即執行。這時,我們就創建了一個宏任務。

在我們加載的代碼中,可能有 click 事件的監聽,也可能會發出網絡請求。當這些操作觸發我們埋下的回調函數後,相應的回調函數都會作爲新的宏任務執行。

當然,在我們的宏任務的代碼中,也可能會用到 Promise,MutationObserver 這些 api,他們的回調函數(或者 then 函數)都會在本輪宏任務結束之後,渲染進程工作之前執行,它們被稱爲微任務。

整個過程如下:

宏任務 => 微任務 => 渲染 => 宏任務 => 微任務 => 渲染 ...

這個循環往復的 過程在頁面生命週期內一直存在,被稱爲 event loop。

一個例子

瞭解了什麼是宏任務和微任務之後,我們來看一個經典的例子:

Promise.resolve().then(() => console.log('promise1 resolved')); // 1
Promise.resolve().then(() => console.log('promise2 resolved')); // 2
setTimeout(() => {
    console.log('set timeout3') // 7
    Promise.resolve().then(() => console.log('inner promise3 resolved')); // 8
}, 0);
setTimeout(() => console.log('set timeout1'), 0); // 9
setTimeout(() => console.log('set timeout2'), 0); // 10
Promise.resolve().then(() => console.log('promise4 resolved')); // 3
Promise.resolve().then(() => {
    console.log('promise5 resolved') // 4
    Promise.resolve().then(() => console.log('inner promise6 resolved'));  // 6
});
Promise.resolve().then(() => console.log('promise7 resolved')); // 5

解釋上面的執行順序:

  1. 每一個 setTimeout 都會放到下一輪宏任務中觸發。
  2. 本輪微任務中產生的新的微任務,會被加到隊尾,仍然在本輪微任務隊列中執行完畢。解釋了 inner promise6 resolved 在 promise7 resolved 後面
  3. 宏任務中產生的微任務,會在該輪宏任務結束之後,統一在微任務隊列中執行,解釋了 inner promise3 resolved 在 set timeout3 後面

於是得到下面的圖,紫色代表宏任務,黃色代表微任務。

觀察可以發現,在第一個宏任務中,除了創建 setTimeout 和 Promise 之外,是沒執行什麼同步代碼的,現在我們在原先的代碼最後再加一行:

...
Promise.resolve().then(() => console.log('promise7 resolved')); // 5
new Promise((reslove) => console.log('promise instance'));

此時,儘管代碼被加在最後一行,promise instance 卻會第一個打印出來,因爲 new Promise 中傳入的函數是立即執行的。也就是說在第一輪宏任務中執行的。

microtask 是會在每一輪 event loop 進行渲染之前會被觸發
且只要在 microtask queue 裡面還有東西的話,就會一直執行下去
直到整個 microtask queue 變成空的為止
也就是說在 microtask 執行的時候,又觸發 queue 新的 microtask 的話
這個新的 microtask 也是會在此輪 task 執行完之前執行,不會留到下一輪 task

Vue 的 nextTick

我們再看下宏任務,微任務,渲染的流程

https://user-gold-cdn.xitu.io/2019/8/21/16cb1d7bb4bd9fd2?imageslim

瀏覽器的 eventloop 是宏任務(dom 更新)=> 微任務 => 渲染,而 Vue 的 DOM 操作是在宏任務中進行的。這就解釋了爲什麼 Vue 在完成本輪 DOM 更新之後,$nextTick 在本輪的微任務中執行回調函數,就可以確保拿到最新 DOM,儘管此時頁面還沒有將最新的 DOM 渲染到瀏覽器上。打開開發者工具,依次執行下面代碼可以證明這一點:

// 模擬上一輪更新狀態
document.body.style.background = 'yellow'; 

接下來模擬執行本輪更新操作:

document.body.style.background = 'red';
Promise.resolve().then(() =>{
    let start = Date.now();
    while(Date.now() - start < 3000) {
        console.log(document.body.style.background) // 由於 while 循環,這裏拿到的 background 已經是最新的 red,但是頁面顯示還是黃色
    }
});
// 微任務執行結束之後,頁面變成紅色

上面的例子說明,在微任務中就可以拿到 Vue 本輪更新的DOM,而此時頁面渲染的未必是最新的DOM。本文完。

參考資料:
https://yu-jack.github.io/2020/02/03/javascript-runtime-event-loop-browser/
https://juejin.im/post/6844903919789801486#heading-4
https://blog.insiderattack.net/javascript-event-loop-vs-node-js-event-loop-aea2b1b85f5c

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