event loop整理

飯特稠發表於2021-02-27

巨集任務和微任務

讓我們從瀏覽器載入 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 後面

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

event loop整理

觀察可以發現,在第一個巨集任務中,除了建立 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://i.iter01.com/images/a31c016f60b385b619728a903b023d29d9d31d07646328b13afd542cfe9b425f.png

瀏覽器的 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

相關文章