web 前端培训: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 不同_web前端培训。
比如,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();
}
高优先级打断低优先级的例子
举例来看一个高优先级打断低优先级的例子:
插入一个低优先级 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 在执行。这就是「时间切片」的原理。
调度系统的实现原理
以下是调度系统的完整实现原理:
对照流程图来看:
文章来源于前端开发
评论