再談談 Promise, setTimeout, rAF, rIC

歡迎關注我的公衆號睿Talk,獲取我最新的文章:
clipboard.png

一、前言

Promise, setTimeout, requestAnimationFrame, requestIdleCallback 這幾個概念相信很多人都很熟悉了,最近在看 React Fiber 源碼的時候又對它們有了更深一層的認識,在此分享一下。下文將用 rAF 代表 requestAnimationFrame, rIC 代表 requestIdleCallback

二、事件循環與幀

事件循環和上面 4 個名詞的基本概念在此不再囉嗦了,我們着重看下它們之間的關係。瀏覽器是一個 UI 系統,所有的操作最終都會以頁面的形式展現,而頁面的基本單位是幀。一幀中可能包括的任務有下面幾種類型。

clipboard.png

  • events: 點擊事件、鍵盤事件、滾動事件等
  • macro: 宏任務,如 setTimeout
  • micro: 微任務,如 Promise
  • rAF: requestAnimationFrame
  • Layout: CSS 計算,頁面佈局
  • Paint: 頁面繪製
  • rIC: requestIdleCallback

理想情況下,頁面會以 60 幀每秒的幀率來運行,但實際上每秒繪製多少幀是由多個因素決定的,下面舉一些例子:

  • 一個加載完成的靜態頁面,當用戶沒有進行交互的情況下,頁面不需要重繪,幀率爲 0。
  • 快速滾動頁面的時候,可視區域的內容不斷髮生變化,瀏覽器會儘可能快的重繪頁面,理想幀率爲 60。
  • 假設頁面有一個註冊了回調的按鈕,回調執行需要 500 毫秒。當點擊按鈕後再快速滾動頁面,頭 500 毫秒頁面是卡住動不了的,後 500 毫秒會儘可能快的重繪頁面,這時候理想幀率爲 30。
  • 當使用 rAF 製作動畫的時候,瀏覽器會儘可能快的重繪頁面,桌面瀏覽器可能是 60 幀,移動瀏覽器可能是 30 幀。

從上面的例子可以看出,頁面的幀率不是固定的,是會動態變化的。比如某一幀中的任務佔據大量時間的情況下,會影響到下一幀的執行。那麼誰來調節幀率呢?顯然只能依靠瀏覽器自身。作爲開發者的我們是無法準確知道回調什麼時候執行的。比如:

function animation() {
   console.log('time: ', +new Date());
   setTimeout(animate, 1000 / 60);
}

animation();

上面的函數是假定瀏覽器以幀率 60 運行的,當幀率達不到的時候,2 幀之間回調可能執行了多次,也可能一次都不執行,簡稱掉幀。

所以在製作動畫的時候,我們不能預設瀏覽器的幀率,正確的做法是通過 rAF 註冊回調, 由瀏覽器來控制動畫調用時機:

function animation() {
   console.log('time: ', +new Date());
   requestAnimationFrame(animation);
}

animation();

rAF 會保證註冊的回調會在下次渲染頁面之前執行,且只會執行一次。當頁面處於不可見狀態時,rAF 會自動停止執行,以節省系統資源。

三、執行順序

Promise, setTimeout , rAFrIC 對應 4 種隊列:微任務隊列、宏任務隊列、animation 隊列和 idle 隊列。

  • 微任務隊列會在 JS 運行棧爲空的時候立即執行。
  • animation 隊列會在頁面渲染前執行。
  • 宏任務隊列優先級相對較低。
  • idle 隊列優先級最低,當瀏覽器有空閒時間的時候纔會執行。
setTimeout(()=>console.log('setTimeout'), 0);
Promise.resolve().then(()=>console.log('promise'));
requestAnimationFrame(()=>console.log('animation'));
requestIdleCallback(()=>console.log('idle'));

// 執行結果: promise, animation, setTimeout, idle

再來談談空閒時間怎麼理解。假設在 1 秒內有 3 幀需要渲染:

clipboard.png

  • 第一幀,由於宏任務佔用了大量的時間,沒有空閒時間。
  • 第二幀,rAF佔用的時間不多,有大量的空閒時間
  • 第三幀,瀏覽器事件佔用的時間不多,有大量的空閒時間

rAF類似,rIC 的執行時機是由瀏覽器控制的,能更好的保證體驗,優化性能。一般高優先級的任務(如 UI 更新)會放在 rAF 隊列,低優先級任務(如日誌上傳)會放 rIC

四、隊列特性

在一個事件循環內,各個隊列有以下特性:

  • 宏任務隊列,每次只會執行隊列內的一個任務。
  • 微任務隊列,每次會執行隊列裏的全部任務。假設微任務隊列內有 100 個 Promise,它們會一次過全部執行完。這種情況下極有可能會導致頁面卡頓。如果在微任務執行過程中繼續往微任務隊列中添加任務,新添加的任務也會在當前事件循環中執行,很容易造成死循環, 如:
function loop() {
    Promise.resolve().then(loop);
}

loop();
  • animation 隊列,跟微任務隊列有點相似,每次會執行隊列裏的全部任務。但如果在執行過程中往隊列中添加新的任務,新的任務不會在當前事件循環中執行,而是在下次事件循環的時候執行。
  • idle 隊列,每次只會執行一個任務。任務完成後會檢查是否還有空閒時間,有的話會繼續執行隊列中的任務,沒有則等到下次有空閒時間再執行。需要注意的是此隊列中的任務也有可能阻塞頁面,當空閒時間用完後任務不會主動退出。如果任務會佔用較長時間,一般會將任務拆分成多個階段,執行完一個階段後檢查還有沒有空閒時間,有則繼續,無則註冊一個新的 idle 隊列任務,然後退出當前任務。React Fiber 就是用這個機制。

五、總結

本文介紹了 4 種隊列的執行順序和每個隊列的特性,它們是:宏任務隊列、微任務隊列、animation 隊列和 idle 隊列。實際應用時可以根據它們各自的特點分配不同的任務。

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