實現(xiàn) React requestIdleCallback 調(diào)度能力
1.前言
Elab掘金: React Fiber架構(gòu)淺析[1] 已對 React Fiber架構(gòu) 實現(xiàn)進行了淺析。React內(nèi)部實現(xiàn)了該方法 requestIdleCallback,即一幀空閑執(zhí)行任務(wù),但Schedular + Lane 模式遠比 requestIdleCallback 復(fù)雜的多。這里我們先通過了解 requestIdleCallback都做了些什么,再嘗試通過 requestAnimationFrame + MessageChannel 來模擬 React 對一幀空閑判斷的實現(xiàn)。
2.requestIdleCallback
window.requestIdleCallback()[2]
2.1 概念理解

圖: 簡單描述幀生命周期
RequestIdleCallback 簡單的說,判斷一幀有空閑時間,則去執(zhí)行某個任務(wù)。
目的是為了解決當(dāng)任務(wù)需要長時間占用主進程,導(dǎo)致更高優(yōu)先級任務(wù)(如動畫或事件任務(wù)),無法及時響應(yīng),而帶來的頁面丟幀(卡死)情況。
故RequestIdleCallback 定位處理的是: 不重要且不緊急的任務(wù)。
RequestIdleCallback 參數(shù)說明:
window.requestIdleCallback(callback[, options]); callback為要執(zhí)行的回調(diào)函數(shù),該函數(shù)會接收deadline作為對象。
//?回調(diào)函數(shù)?接收?deadline
type?Deadline?=?{
? timeRemaining:?()?=> number //?當(dāng)前剩余的可用時間。即該幀剩余時間。
? didTimeout: boolean //?是否超時。
}
//?接收回調(diào)任務(wù)
type?RequestIdleCallback?=?(cb:?(deadline:?Deadline)?=>?void,?options?:?Options)?=>?number?
2.2 實現(xiàn)demo
requestIdleCallback 處理任務(wù)說明:
Demo: https://linjiayu6.github.io/FE-RequestIdleCallback-demo/
Github: RequestIdleCallback 實驗[3]

const?bindClick?=?id?=>?
??element(id).addEventListener('click',?Work.onAsyncUnit)
//?綁定click事件
bindClick('btnA')
bindClick('btnB')
bindClick('btnC')
var?Work?=?{
????//?有1萬個任務(wù)
????unit:?10000,
????//?處理單個任務(wù)需要處理如下
????onOneUnit:?function?()?{??for?(var?i?=?0;?i?<=?500000;?i++)?{}?},
????
????//?處理任務(wù)
????onAsyncUnit:?function?()?{
????????//?空閑時間基準為?1ms
????????const?FREE_TIME?=?1
????????//?執(zhí)行到第幾個任務(wù)
????????let?_u?=?0
????????function?cb(deadline)?{
????????????//?當(dāng)任務(wù)還沒有被處理完?&?一幀還有的空閑時間?>?1ms
????????????while?(_u??FREE_TIME)?{
????????????????Work.onOneUnit()
????????????????_u?++
????????????}
????????????//?任務(wù)干完,?執(zhí)行回調(diào)
????????????if?(_u?>=?Work.unit)?{
????????????????//?執(zhí)行回調(diào)
????????????????return
????????????}
????????????//?任務(wù)沒完成,?繼續(xù)等空閑執(zhí)行
????????????window.requestIdleCallback(cb)
????????}
????????window.requestIdleCallback(cb)
????}
}
以上是 window.requestIdleCallback 的實現(xiàn)流程。
核心: 即瀏覽器去在一幀有空閑的情況下,去執(zhí)行某個低優(yōu)先級的任務(wù)。
2.3 缺陷
MAY BE OFFTOPIC: requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work. requestAnimationFrame is called more often, but specific for the task which name suggests.[4]
實驗 api,兼容情況一般。 實驗結(jié)論: requestIdleCallback FPS只有20ms,正常情況下渲染一幀時長控制在16.67ms (1s / 60 = 16.67ms)。該時間是高于頁面流暢的訴求。 個人認為: RequestIdleCallback 不重要且不緊急的定位。因為React渲染內(nèi)容,并非是不重要且不緊急。不僅該api兼容一般,幀渲染能力一般,也不太符合渲染訴求,故React 團隊自行實現(xiàn)。
3.React requestIdleCallback 實現(xiàn)實驗
想要實現(xiàn)requestIdleCallback的處理,有2個點需要解決:
When: 如何判斷一幀是否有空閑? Where: 如果有了空閑,在一幀中哪里去執(zhí)行任務(wù)?
3.1 requestAnimationFrame 計算一幀到期時間點
requestAnimationFrame[5]
是由系統(tǒng)來決定回調(diào)函數(shù)的執(zhí)行時機。 它會把每一幀中的所有DOM操作集中起來,在一次重繪或回流中就完成,并且重繪或回流的時間間隔緊緊跟隨屏幕的刷新頻率,不會引起丟幀和卡頓。
瀏覽器刷新率在60Hz, 渲染一幀時長控制在16.67ms (1s / 60 = 16.67ms)。
DOMHighResTimeStamp[6]
requestAnimationFrame 參數(shù)如下:
//?回調(diào)函數(shù)?接收?rafTime?即?開始執(zhí)行一幀的開始時間
//?接收回調(diào)任務(wù)
type?RequestAnimationFrame?=?(cb:?(rafTime:?number)?=>?void)
計算一幀用到期的時間點。
//?計算出當(dāng)前幀?結(jié)束時間
var?deadlineTime;
window.selfRequestIdleCallback?=?function?(cb)?{
????requestAnimationFrame(rafTime?=>?{
????????//?結(jié)束時間?=?開始時間?+?一幀用時16.667ms
????????deadlineTime?=?rafTime?+?16.667
????????//?......?
????})
}
以上使用 requestAnimationFrame 來計算結(jié)束的時間點。
我們暫且將空閑時間的判斷放到后面去解決,先來看在時間充裕情況下,在什么時機去執(zhí)行某任務(wù)。
3.2 MessageChannel 宏任務(wù) 執(zhí)行任務(wù)
MessageChannel()[7]
MessageChannel創(chuàng)建了一個通信的管道,這個管道有兩個端口,每個端口都可以通過postMessage發(fā)送數(shù)據(jù),而一個端口只要綁定了onmessage回調(diào)方法,就可以接收從另一個端口傳過來的數(shù)據(jù)。
在看著方法實現(xiàn)之前,你可能有疑問:
為什么使用宏任務(wù)處理呢?
核心是將主進程讓出,將瀏覽器去更新頁面。
利用事件循環(huán)機制,在下一幀宏任務(wù)的時候,執(zhí)行未完成的任務(wù)。
為什么不是微任務(wù)?
走遠了。對一個事件循環(huán)機制來說,在頁面更新前,會將所有的微任務(wù)全部執(zhí)行完,故無法達成將主線程讓出給瀏覽器的目的。
既然用了宏任務(wù),那為什么不使用 setTimeout 宏任務(wù)執(zhí)行呢?
如果不支持MessageChannel的話,就會去用 setTimeout 來執(zhí)行,只是退而求其次的辦法。 現(xiàn)實情況是: 瀏覽器在執(zhí)行 setTimeout()和setInterval()時,會設(shè)定一個最小的時間閾值,一般是 4ms。
var?i?=?0
var?_start?=?+new?Date()
function?fn()?{
??setTimeout(()?=>?{
????console.log("執(zhí)行次數(shù),?時間",?++i,?+new?Date()?-?_start)
????if?(i?===?10)?{
??????return
????}
????fn()
??},?0)
}
fn()

故,利用MessageChannel來執(zhí)行宏任務(wù),且模擬setTimeout(fn, 0),還沒有時延哦。
實現(xiàn)如下:
//?計算出當(dāng)前幀?結(jié)束時間點
var?deadlineTime
//?保存任務(wù)
var?callback
//?建立通信
var?channel?=?new?MessageChannel()
var?port1?=?channel.port1;
var?port2?=?channel.port2;
//?接收并執(zhí)行宏任務(wù)
port2.onmessage?=?()?=>?{
????//?判斷當(dāng)前幀是否還有空閑,即返回的是剩下的時間
????const?timeRemaining?=?()?=>?deadlineTime?-?performance.now();
????const?_timeRemain?=?timeRemaining();
????//?有空閑時間?且?有回調(diào)任務(wù)
????if?(_timeRemain?>?0?&&?callback)?{
????????const?deadline?=?{
????????????timeRemaining,?//?計算剩余時間
????????????didTimeout:?_timeRemain?0?//?當(dāng)前幀是否完成
????????}
????????//?執(zhí)行回調(diào)
????????callback(deadline)
????}
}
window.requestIdleCallback?=?function?(cb)?{
????requestAnimationFrame(rafTime?=>?{
????????//?結(jié)束時間點?=?開始時間點?+?一幀用時16.667ms
????????deadlineTime?=?rafTime?+?16.667
????????//?保存任務(wù)
????????callback?=?cb
????????//?發(fā)送個宏任務(wù)
????????port1.postMessage(null);
????})
}
4.React 源碼 requestHostCallback
SchedulerHostConfig.js[8]
執(zhí)行宏任務(wù)(回調(diào)任務(wù))
requestHostCallback: 觸發(fā)一個宏任務(wù) performWorkUntilDeadline。
performWorkUntilDeadline: 宏任務(wù)處理。
是否有富裕時間, 有則執(zhí)行。 執(zhí)行該回調(diào)任務(wù)后,是否還有下一個回調(diào)任務(wù), 即判斷 hasMoreWork。 有則繼續(xù)執(zhí)行 port.postMessage(null);
??let?scheduledHostCallback?=?null;
??let?isMessageLoopRunning?=?false;
?
??const?channel?=?new?MessageChannel();
??//?port2?發(fā)送
??const?port?=?channel.port2;
??//?port1?接收
??channel.port1.onmessage?=?performWorkUntilDeadline;
??const?performWorkUntilDeadline?=?()?=>?{
????//?有執(zhí)行任務(wù)
????if?(scheduledHostCallback?!==?null)?{
??????const?currentTime?=?getCurrentTime();
??????//?Yield?after?`yieldInterval`?ms,?regardless?of?where?we?are?in?the?vsync
??????//?cycle.?This?means?there's?always?time?remaining?at?the?beginning?of
??????//?the?message?event.
??????//?計算一幀的過期時間點
??????deadline?=?currentTime?+?yieldInterval;
??????const?hasTimeRemaining?=?true;
??????try?{
????????//?執(zhí)行完該回調(diào)后,?判斷后續(xù)是否還有其他任務(wù)
????????const?hasMoreWork?=?scheduledHostCallback(
??????????hasTimeRemaining,
??????????currentTime,
????????);
????????if?(!hasMoreWork)?{
??????????isMessageLoopRunning?=?false;
??????????scheduledHostCallback?=?null;
????????}?else?{
??????????//?If?there's?more?work,?schedule?the?next?message?event?at?the?end
??????????//?of?the?preceding?one.
??????????//?還有其他任務(wù),?推進進入下一個宏任務(wù)隊列中
??????????port.postMessage(null);
????????}
??????}?catch?(error)?{
????????//?If?a?scheduler?task?throws,?exit?the?current?browser?task?so?the
????????//?error?can?be?observed.
????????port.postMessage(null);
????????throw?error;
??????}
????}?else?{
??????isMessageLoopRunning?=?false;
????}
????//?Yielding?to?the?browser?will?give?it?a?chance?to?paint,?so?we?can
????//?reset?this.
????needsPaint?=?false;
??};
??//?requestHostCallback?一幀中執(zhí)行任務(wù)
??requestHostCallback?=?function(callback)?{
????//?回調(diào)注冊
????scheduledHostCallback?=?callback;
????if?(!isMessageLoopRunning)?{
??????isMessageLoopRunning?=?true;
??????//?進入宏任務(wù)隊列
??????port.postMessage(null);
????}
??};
??cancelHostCallback?=?function()?{
????scheduledHostCallback?=?null;
??};
參考資料
Elab掘金: React Fiber架構(gòu)淺析: https://juejin.cn/post/7005880269827735566
[2]window.requestIdleCallback(): https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
[3]RequestIdleCallback 實驗: https://github.com/Linjiayu6/FE-RequestIdleCallback-demo
[4]MAY BE OFFTOPIC: requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work. requestAnimationFrame is called more often, but specific for the task which name suggests.: https://github.com/facebook/react/issues/13206#issuecomment-418923831
[5]requestAnimationFrame: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
[6]DOMHighResTimeStamp: https://developer.mozilla.org/zh-CN/docs/Web/API/DOMHighResTimeStamp
[7]MessageChannel(): https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel/MessageChannel
[8]SchedulerHostConfig.js: https://github.com/facebook/react/blob/v17.0.1/packages/scheduler/src/forks/SchedulerHostConfig.default.js

往期推薦



最后
歡迎加我微信,拉你進技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認真學(xué)前端,做個專業(yè)的技術(shù)人...


