React源碼Scheduler(一)瀏覽器的調度

NO IMAGE

本文源碼基於 React 16.8.6 (March 27, 2019),僅記錄一些個人閱讀源碼的分享與體會。

歡迎大家交流和探討

背景

Schedule 即任務的調度,我們知道 JavaScript 是單線程運行的。因此,瀏覽器無法同時相應 JS 任務與用戶的 UI 操作,如此在執行 UI 操作的時候,便會帶給用戶一定卡頓感,也就是我們所謂的「丟幀」。

對此情況,React 採用的是時間分片的策略,將任務細化為不同優先級,利用瀏覽器的空閒時間進行任務的執行以保證 UI 操作的流暢。瀏覽器的調度 API 主要分為兩種,分別是高優先級的 requestAnimationFrame 與低優先級的 requestIdleCallback

RequestAnimationFrame

requestAnimationFrame 在每一幀的開始階段執行,一般用來進行復雜動畫的繪製。該函數接受一個接收 DOMHighResTimeStamp 參數的 callback 函數作為參數,返回一個 requestIdcancelAnimationFrame 以取消。

由於該函數每幀開始必執行,因此我們可以基於此,在每幀開始時執行一定任務,實現一個簡單的時間分片調度。

// create 1000 tasks 
const tasks = Array.from({ length: 1000 }, () => () => { console.log('task run'); })
const doTasks = (fromIndex = 0) => {
const start = Date.now();
let i = fromIndex;
let end;
do {
tasks[i++](); // do task
end = Date.now();
} while(i < tasks.length && end - start < 20); // Do tasks in 20ms
console.log('tasks remain: ', 1000 - i);
// if remaining tasks exsis when timeout. Run at next frame
if (i < tasks.length) {
requestAnimationFrame(doTasks.bind(null, i));
}
}
// start tasks scheduler
requestAnimationFrame(doTasks.bind(null, 0))
/** 
output:
168 task run
tasks remain:  832
178 task run
asks remain:  654
162 task run
tasks remain:  492
119 task run
tasks remain:  373
158 task run
tasks remain:  215
87 task run
tasks remain:  128
125 task run
tasks remain:  3
3 task run
tasks remain:  0
*/

我們可以看到,通過 requestAnimationFrame 的調度,我們實現了一個簡單的時間分片功能,在每幀留出 20ms 進行 js 的任務執行。但這時候就引入一個問題:20ms 是如何確定的?如果一個時間點任務實際需要耗時小於 20ms,那多出的時間豈不是浪費了?為了解決這個問題,就引出了我們的第二個調度 API: requestIdleCallback

RequestIdleCallback

與每幀執行的 requestAnimationFrame 相對,requestIdleCallback 是一個低優先級調度,當且僅當瀏覽器空閒時才會執行任務的調度。這就解決了之前例子裡如何確定任務應該執行時間這一問題。requestIdleCallback 接收兩個參數。第一個參數為接受一個 IdleDeadline參數的 callback 函數,第二個參數為可選的 options,包含一個 timeout 配置項,指定該回調的超時時間,以保證任務不至於餓死。由此,我們便可基於此對上述代碼進行修改。

const tasks = Array.from({ length: 1000 }, () => () => { console.log('task run'); })
const doTasks = (fromIndex = 0, idleDeadline) => {
let i = fromIndex;
let end;
console.log('time remains: ', idleDeadline.timeRemaining());
do {
tasks[i++](); // do task
} while(i < tasks.length && idleDeadline.timeRemaining() > 0); // Do tasks in 20ms
console.log('tasks remain: ', 1000 - i);
// if remaining tasks exsis when timeout. Run at next frame
if (i < tasks.length) {
requestIdleCallback(doTasks.bind(null, i));
}
}
// start tasks scheduler
requestIdleCallback(doTasks.bind(null, 0))
/**
output:
time remains:  49.970000000000006
360 task run
tasks remain:  640
time remains:  49.77
395 task run
tasks remain:  245
time remains:  29.255000000000003
215 task run
tasks remain:  30
time remains:  49.96000000000001
30 task run
tasks remain:  0
*/

第二個版本的代碼,我們通過 idleDeadline.timeRemaining() 獲取當前剩餘時間進行任務的調度。在複雜情況下,會出現瀏覽器空閒時間過少導致任務堆積問題,這時候第二個參數的 timeout 配置就派上用場了。有興趣的小夥伴可以自己試試。

在 React 中的任務調度,也採用了 requestIdleCallback 實現調度,但由於該 API 的兼容性問題(Safari 這個新生代的 IE),React 內部自己基於 requestAnimationFrame 實現了一個 requestIdleCallback 的 polyfill。我們將在下一篇中進行介紹。

相關文章

學習設計模式前傳

函數式編程在前端權限管理中的應用

React源碼Scheduler(三)React的調度算法實現

React源碼Scheduler(二)React的調度流程