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

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

歡迎你們交流和探討java

背景

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

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

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: requestIdleCallbackui

RequestIdleCallback

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

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。咱們將在下一篇中進行介紹。code

相關文章
相關標籤/搜索