大家好,我卡頌。
想必大家都知道React
有一套基於Fiber
架構的排程系統。
這套排程系統的基本功能包括:
- 更新有不同優先順序
- 一次更新可能涉及多個元件的
render
,這些render
可能分配到多個巨集任務
中執行(即時間切片
) 高優先順序更新
會打斷進行中的低優先順序更新
本文會用100行程式碼實現這套排程系統,讓你快速瞭解React
的排程原理。
我知道你不喜歡看大段的程式碼,所以本文會以圖
+程式碼片段
的形式講解原理。
文末有完整的線上Demo
,你可以自己上手玩玩。
開整!
歡迎加入人類高質量前端框架群,帶飛
準備工作
我們用work
這一資料結構代表一份工作,work.count
代表這份工作要重複做某件事的次數。
在Demo
中要重複做的事是“執行insertItem
方法,向頁面插入<span/>
”:
const insertItem = (content: string) => {
const ele = document.createElement('span');
ele.innerText = `${content}`;
contentBox.appendChild(ele);
};
所以,對於如下work
:
const work1 = {
count: 100
}
代表:執行100次insertItem
向頁面插入100個<span/>
。
work
可以類比React
的一次更新
,work.count
類比這次更新要render的元件數量
。所以Demo
是對React
更新流程的類比
來實現第一版的排程系統,流程如圖:
包括三步:
- 向
workList
佇列(用於儲存所有work
)插入work
schedule
方法從workList
中取出work
,傳遞給perform
perform
方法執行完work
的所有工作後重復步驟2
程式碼如下:
// 儲存所有work的佇列
const workList: work[] = [];
// 排程
function schedule() {
// 從佇列尾取一個work
const curWork = workList.pop();
if (curWork) {
perform(curWork);
}
}
// 執行
function perform(work: Work) {
while (work.count) {
work.count--;
insertItem();
}
schedule();
}
為按鈕繫結點選互動,最基本的排程系統就完成了:
button.onclick = () => {
workList.unshift({
count: 100
})
schedule();
}
點選button
就能插入100個<span/>
。
用React
類比就是:點選button
,觸發同步更新,100個元件render
接下來我們將其改造成非同步的。
Scheduler
React
內部使用Scheduler完成非同步排程。
Scheduler
是獨立的包。所以可以用他改造我們的Demo
。
Scheduler
預置了5種優先順序,從上往下優先順序降低:
ImmediatePriority
,最高的同步優先順序UserBlockingPriority
NormalPriority
LowPriority
IdlePriority
,最低優先順序
scheduleCallback
方法接收優先順序
與回撥函式fn
,用於排程fn
:
// 將回撥函式fn以LowPriority優先順序排程
scheduleCallback(LowPriority, fn)
在Scheduler
內部,執行scheduleCallback
後會生成task
這一資料結構:
const task1 = {
expiration: startTime + timeout,
callback: fn
}
task1.expiration
代表task1
的過期時間,Scheduler
會優先執行過期的task.callback
。
expiration
中startTime
為當前開始時間,不同優先順序的timeout
不同。
比如,ImmediatePriority
的timeout
為-1,由於:
startTime - 1 < startTime
所以ImmediatePriority
會立刻過期,callback
立刻執行。
而IdlePriority
對應timeout
為1073741823(最大的31位帶符號整型),其callback
需要非常長時間才會執行。
callback
會在新的巨集任務
中執行,這就是Scheduler
排程的原理。
用Scheduler改造Demo
改造後的流程如圖:
改造前,work
直接從workList
佇列尾取出:
// 改造前
const curWork = workList.pop();
改造後,work
可以擁有不同優先順序
,通過priority
欄位表示。
比如,如下work
代表以NormalPriority優先順序插入100個\<span/\>:
const work1 = {
count: 100,
priority: NormalPriority
}
所以,改造後每次都使用最高優先順序的work
:
// 改造後
// 對workList排序後取priority值最小的(值越小,優先順序越高)
const curWork = workList.sort((w1, w2) => {
return w1.priority - w2.priority;
})[0];
改造後流程的變化
由流程圖可知,Scheduler
不再直接執行perform
,而是通過執行scheduleCallback
排程perform.bind(null, work)
。
即,滿足一定條件的情況下,生成新task
:
const someTask = {
callback: perform.bind(null, work),
expiration: xxx
}
同時,work
的工作也是可中斷的。在改造前,perform
會同步執行完work
中的所有工作:
while (work.count) {
work.count--;
insertItem();
}
改造後,work
的執行流程隨時可能中斷:
while (!needYield() && work.count) {
work.count--;
insertItem();
}
needYield
方法的實現(何時會中斷)請參考文末線上Demo
高優先順序打斷低優先順序的例子
舉例來看一個高優先順序
打斷低優先順序
的例子:
- 插入一個低優先順序
work
,屬性如下
const work1 = {
count: 100,
priority: LowPriority
}
- 經歷
schedule
(排程),perform
(執行),在執行了80次工作時,突然插入一個高優先順序work
,此時:
const work1 = {
// work1已經執行了80次工作,還差20次執行完
count: 20,
priority: LowPriority
}
// 新插入的高優先順序work
const work2 = {
count: 100,
priority: ImmediatePriority
}
work1
工作中斷,繼續schedule
。由於work2
優先順序更高,會進入work2
對應perform
,執行100次工作work2
執行完後,繼續schedule
,執行work1
剩餘的20次工作
在這個例子中,我們需要區分2個打斷的概念:
- 在步驟3中,
work1
執行的工作被打斷。這是微觀角度的打斷 - 由於
work1
被打斷,所以繼續schedule
。下一個執行工作的是更高優的work2
。work2
的到來導致work1
被打斷,這是巨集觀角度的打斷
之所以要區分巨集/微觀,是因為微觀的打斷不一定意味著巨集觀的打斷。
比如:work1
由於時間切片用盡,被打斷。沒有其他更高優的work
與他競爭schedule
的話,下一次perform
還是work1
。
這種情況下微觀下多次打斷,但是巨集觀來看,還是同一個work
在執行。這就是時間切片的原理。
排程系統的實現原理
以下是排程系統的完整實現原理:
對照流程圖來看:
總結
本文是React
排程系統的簡易實現,主要包括兩個階段:
- schedule
- perform
這裡是完整Demo地址。