Schedule
react有一套基于Fiber架构的调度系统,这套调度系统的基本功能包括:
-
1 更新具有不同的优先级
-
2 一次根因可能涉及多个组件的render,这些render可能分配到多个宏任务中执行(即时间切片)
-
3 搞优先级更新打断低优先级更新
实现第一版的调度系统,如图:
(图借于魔术师卡颂)
可以看到是同步的,workList存放所有的work,然后schedule取出后,给perform执行,执行完后继续执行schdeule,递归直到所有work执行完毕。
// 用work数据结果代表一份工作,work.count代表这份工作重复做的次数
// 在Demo中需要做的事情就是执行insertItem
interface Work {
count: number;
priority?: number
}
const insertItem = (content: string) => {
const ele = document.createElement("span");
ele.innerText = `${content}`;
document.body.appendChild(ele);
};
const work1: Work = {
count: 100,
};
执行100次insertItem向页面插入100个。
work可以类比React的一次更新,work.count类比这次更新要render的组件数量,一共有100个组件需要更新。所以Demo是对React更新流程的类比
实现各个方法:
// 工作列表
const workList: Work[] = [];
// 调度
function schedule() {
// 队尾取出一个
const curWork = workList.pop();
if (curWork) {
perform(curWork);
}
}
// 执行 perform
function perform(work: Work) {
while (work.count) {
work.count--;
insertItem(work.count + "");
}
// 执行完毕继续调度
schedule();
}
// 可以看到主要分为三步: 1 向workList队列插入work 2 schedule从队列取出work,传给perform执行 3 执行完毕后继续执行schedule方法,知道所有work执行完毕。
const button = document.getElementById('button')
button.onclick = () => {
workList.push(work1)
schedule()
}
// 点击button就可以i插入100个span,用React类比就是:点击button,触发同步更新,100个组件render
这是同步的做法,接着改造成如今Schedule的步骤。
Schedule
他是一个单独的包,使用它来改造demo。
原理就是:预置了5种优先级,给每一个任务赋值优先级,每个优先级对应各自的timeout,当任务过期,该任务的回调函数会立马同步执行 ,回调函数会在新的宏任务中执行。
了解Schedule包的一些内容
优先级
ImmediatePriority,最高的同步优先级
UserBlockingPriority
NormalPriority
LowPriority
IdlePriority,最低优先级
从上到小分别对应1,2,3,4,5,值越低,优先级越高。
scheduleCallback函数 创建task对象,根据优先级赋值过期时间等。
scheduleCallback函数接受三个入参,一般传入前两个,优先级和fn。如上,根据传入的优先级,赋值不同的timeout。
执行scheduleCallback函数会创建task对象。
如上,expirationTime代表这个task的过期时间,Schdeule会优先执行过期的task.callback。startTIme为当前开始时间,不同优先级的timeout不同。
如ImmediatePriority的优先级为-1,所以expirationTime比startTime还短,已经过期,必须立刻执行callback。
shouldYield
只需了解shouldYield是用来告诉我们当前浏览器是否有多余的时间让我们继续执行js。
getFirstCallbackNode
只需了解这个函数是用来获取当前正在调度的work。
cancelCallback
只需了解,这个函数是用来取消传入的task。
了解完Schedule的前置知识,现在用Schedule打造demo。
先创建每个优先级对应的按钮
import {
unstable_IdlePriority as IdlePriority,
unstable_ImmediatePriority as ImmediatePriority,
unstable_LowPriority as LowPriority,
unstable_NormalPriority as NormalPriority,
unstable_UserBlockingPriority as UserBlockingPriority,
unstable_getFirstCallbackNode as getFirstCallbackNode,
unstable_scheduleCallback as scheduleCallback,
unstable_shouldYield as shouldYield,
unstable_cancelCallback as cancelCallback,
CallbackNode
} from "scheduler";
// 对应按钮
const priority2UseList: Priority[] = [
ImmediatePriority, //1
UserBlockingPriority, //2
NormalPriority, // 3
LowPriority //4
];
const priority2Name = [
"noop",
"ImmediatePriority",
"UserBlockingPriority",
"NormalPriority",
"LowPriority",
"IdlePriority"
];
// 初始化优先级对应按钮
priority2UseList.forEach((priority) => {
const btn = document.createElement("button");
root.appendChild(btn);
btn.innerText = priority2Name[priority];
btn.onclick = () => {
// 插入工作
workList.push({
priority,
count: 100
});
schedule();
};
});
// 插每次插入一个span,就延时一会。
const insertItem = (content: string) => {
const ele = document.createElement("span");
ele.innerText = `${content}`;
ele.className = `pri-${content}`;
doSomeBuzyWork(10000000);
contentBox.appendChild(ele);
};
const doSomeBuzyWork = (len: number) => {
let result = 0;
while (len--) {
result += len;
}
};
如上,当点击任一按钮,schedule开始工作。
改造schedule
let prevPriority: Priority = IdlePriority; //当前工作的work的优先级
let curCallback: CallbackNode | null; //当前工作wrok
function schedule(){
// 当前可能存在正在调度的回调
const currentNode = getFirstCallbackNode() // 获取当前工作的work
// 取出当前workList中任务优先级最高的任务
const curWork = workList.sort((w1, w2) => {
return w1.priority - w2.priority;
})[0];
if(!curWork){
//没有工作需要调度,退出调度
curCallback = null
}
const { priority: curPriority } = curWork; //当前的最高优先级
if (curPriority === prevPriority) {
// 有工作在进行,比较该工作与正在进行的工作的优先级
// 如果优先级相同,则不需要调度新的,退出调度
return;
}
// 准备调度当前最高优先级的工作
// 调度之前,如果有工作在进行,则中断他
currentNode && cancelCallback(currentNode); // cancelCallback取消cancelCallback的任务
// 开始调度 当前最高优先级的工作
curCallback = scheduleCallback(curPriority, perform.bind(null, curWork));
//curCallback就是scheduleCallback返回的newTask对象
}
每次调度进行都会获取当前工作的work,然后从workList中获取优先级最高的任务,去比较,如果不存在或者优先级一样,那就不需要优先调度,等待当前任务调度完即会调度。而如果是优先级较高的,就会停止当前的任务,然后调度搞优先级的任务。
改造perform
// scheduleCallback注册回调函数后,执行的时候会传入didTimeout,表示该任务是否过期
function perform(work: Work, didTimeout?:boolean): any {
// 是否需要同步执行
const needSync = work.priority === ImmediatePriority || didTimeout //如果优先级是同步优先级,或者是已经超时了
while ((needSync || !shouldYield()) && work.count) {// shouldYield用来判断当前桢是否有多余时间。
// 如果当前是紧急任务,或者是当前shouldYield返回fasle,即当前桢有多余时间,并且有任务,就执行一次任务
// 当当前桢没时间的时候,就不执行,继续往下判断调度。(异步中断的原理)
work.count--;
// 执行具体的工作
insertItem(work.priority + "");
}
prevPriority = work.priority; //当前执行任务的优先级
//如果当前work完成了
if (!work.count) {
// 完成的work,从workList中删除
const workIndex = workList.indexOf(work);
workList.splice(workIndex, 1);
// 重置优先级
prevPriority = IdlePriority;
}
// 当前work不需要同步执行的
const prevCallback = curCallback; //保存上一个调度
// 调度完后,如果callback变化,代表这是新的work
schedule(); //继续调度
const newCallback = curCallback; //最新调度的任务
//如果上一个调度的任务跟最新调度任务一样
if (newCallback && prevCallback === newCallback) {
// callback没变,代表是同一个work,只不过时间切片时间用尽(5ms)
// 返回的函数会被Scheduler继续调用
return perform.bind(null, work);
}
}
perform每次执行的时候,如果当前的任务已经过期,就表示应该立即执行,或者是当前桢有剩余时间的时候就会执行一次任务,如果是已经超时,则不管当前桢是否有剩余时间,即使卡顿也要执行完成,然后当当前桢没时间的时候,就中断,继续往下执行schedule函数,查看是否有更高优先级的任务,这个是实现可中断的异步更新的关键,当获取到更高优先级的任务,就会优先执行更高优先级的任务。
比如
有一个低优先级的任务
const work1 = {
count: 100,
priority: LowPriority
}
经过schedule调度,perfrom执行了80次后,
const work1 = {
// work1已经执行了80次工作,还差20次执行完
count: 20,
priority: LowPriority
}
来了一个更高优先级的任务
// 新插入的高优先级work
const work2 = {
count: 100,
priority: ImmediatePriority
}
而且work1的过期时间还没到,那么再80次执行完后调度的时候,获取到了更高优先级的任务,所以转过头去执行work2了,等work2执行完毕后,才继续执行剩余的work1。
这里work1会被打断,打断有两个概念:1 因为更高优先级任务被打断 2 当前桢时间不够被打断。
对于第一种,下一次执行perfrom函数的就是新的work了,而对于第二种,下次执行perform函数就还是老的work。
function perform(work, didTimeout){
//...
// 当前work不需要同步执行的
const prevCallback = curCallback; //保存上一个调度
// 调度完后,如果callback变化,代表这是新的work
schedule(); //继续调度
const newCallback = curCallback; //最新调度的任务
//如果上一个调度的任务跟最新调度任务一样
if (newCallback && prevCallback === newCallback) {
// callback没变,代表是同一个work,只不过时间切片时间用尽(5ms)
// 返回的函数会被Scheduler继续调用
return perform.bind(null, work);
}
}
借用两张图(来自卡颂):
调度系统的实现原理
总结:
-
Schedule定义了几个优先级概念,值越低优先级越高,提供了scheduleCallback函数用来已不同的优先级注册函数。每次执行Schedul会从当前所有的任务中,挑选出优先级最高的任务,在开始调度前,如果有正在执行的调度,并且正在执行的调度的优先级比较低的时候,必须调用cancelCallback中断当前的任务,开始调度高优先级的任务。
-
调度执行对应任务的perform方法,如果该任务已经超时,或者属于ImmediatePritority(同步执行优先级),则不管当前桢限制,必须立马执行他。否则该任务会根据当前桢的剩余时间来决定是否继续执行。
-
当任务没执行完成的时候,他不会从当前堆中去除,如果没有更高优先级任务的时候,如果是因为时间切片限制,即当前桢剩余时间不够,那么下次调度还是获取到当前的任务继续执行。当任务执行完毕后,就会从当前堆中去掉,下次调度就取下一个优先级较低的任务执行
-
而可中断的异步更新,就是在当当前任务执行的时候,由于当前桢时间不够,被中断了,然后继续调度的时候,schedule发现了更高优先级的任务,此时,下次执行perfrom的任务就是这个高优先级的任务了。上一个未完成的任务被中断,等到高优先级的任务完成之后才会继续往下完成。
看卡老师的demo地址:
如图,当我们点击低优先级,然后再点击高优先级的时候,比如渲染4,然后点击了3,他就会停止渲染4,开始渲染3,等3渲染完之后再渲染4。而最后的案例,点击3之后再点击4,可以看到,4因为优先级比较低,他不会中断3的渲染,而是等待3渲染完之后再去渲染4。
还有一个特点:
demo演示
那就当渲染的是ImmediatePriority的任务的时候,他是属于同步任务,所以他不需要判断当前桢是否有剩余时间才能执行,而是一口气执行完毕。而点击4,因为4的优先级比较低,他会根据当前桢来判断执不执行函数,所以看起来比较流畅。
而执行2的时候,一开始流畅,后面卡顿,是因为2的优先级也比较高,timeout比较短,一开始他并不超时,所以会根据当前桢的剩余时间执行,不会影响渲染线程,而当执行完一段时间后,该任务超时了,那么他就不管当前桢是否有剩余时间,而是一口气执行完毕,导致gui线程无法工作,所以就造成页面卡顿的现象。
这也是react实现可中断的异步更新的原理。因为Schedule和Reconciler模块的工作是在内存当中运行的,并且它可以中断,而用户完全不会感知到,因为只有当所有的组件Reconciler完毕,才会叫个Renderer模块进行渲染。
学习文章来自:https://zhuanlan.zhihu.com/p/446006183
本文仅学习笔记使用。