大家好,我卡颂。
想必大家都知道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地址。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。