100行代码实现React核心调度功能
source link: https://segmentfault.com/a/1190000041126142
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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个\<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地址。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK