100 行代碼實現 React 核心調度功能
大家好,我卡頌。
想必大家都知道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 個 」**:
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 地址。
參考資料
[1]
Scheduler: https://github.com/facebook/react/tree/main/packages/scheduler
[2]
完整 Demo 地址: https://codesandbox.io/s/xenodochial-alex-db74g?file=/src/index.ts
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/uuxHlanqyN2HneYOz7DMVw