歡迎關注我的公衆號睿Talk
,獲取我最新的文章:
一、前言
Promise
, setTimeout
, requestAnimationFrame
, requestIdleCallback
這幾個概念相信很多人都很熟悉了,最近在看 React Fiber
源碼的時候又對它們有了更深一層的認識,在此分享一下。下文將用 rAF
代表 requestAnimationFrame
, rIC
代表 requestIdleCallback
。
二、事件循環與幀
事件循環和上面 4 個名詞的基本概念在此不再囉嗦了,我們着重看下它們之間的關係。瀏覽器是一個 UI 系統,所有的操作最終都會以頁面的形式展現,而頁面的基本單位是幀。一幀中可能包括的任務有下面幾種類型。
- 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
, rAF
和 rIC
對應 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 幀需要渲染:
- 第一幀,由於宏任務佔用了大量的時間,沒有空閒時間。
- 第二幀,
rAF
佔用的時間不多,有大量的空閒時間 - 第三幀,瀏覽器事件佔用的時間不多,有大量的空閒時間
與rAF
類似,rIC
的執行時機是由瀏覽器控制的,能更好的保證體驗,優化性能。一般高優先級的任務(如 UI 更新)會放在 rAF
隊列,低優先級任務(如日誌上傳)會放 rIC
。
四、隊列特性
在一個事件循環內,各個隊列有以下特性:
- 宏任務隊列,每次只會執行隊列內的一個任務。
- 微任務隊列,每次會執行隊列裏的全部任務。假設微任務隊列內有 100 個 Promise,它們會一次過全部執行完。這種情況下極有可能會導致頁面卡頓。如果在微任務執行過程中繼續往微任務隊列中添加任務,新添加的任務也會在當前事件循環中執行,很容易造成死循環, 如:
function loop() {
Promise.resolve().then(loop);
}
loop();
- animation 隊列,跟微任務隊列有點相似,每次會執行隊列裏的全部任務。但如果在執行過程中往隊列中添加新的任務,新的任務不會在當前事件循環中執行,而是在下次事件循環的時候執行。
- idle 隊列,每次只會執行一個任務。任務完成後會檢查是否還有空閒時間,有的話會繼續執行隊列中的任務,沒有則等到下次有空閒時間再執行。需要注意的是此隊列中的任務也有可能阻塞頁面,當空閒時間用完後任務不會主動退出。如果任務會佔用較長時間,一般會將任務拆分成多個階段,執行完一個階段後檢查還有沒有空閒時間,有則繼續,無則註冊一個新的 idle 隊列任務,然後退出當前任務。
React Fiber
就是用這個機制。
五、總結
本文介紹了 4 種隊列的執行順序和每個隊列的特性,它們是:宏任務隊列、微任務隊列、animation 隊列和 idle 隊列。實際應用時可以根據它們各自的特點分配不同的任務。