一文搞懂react的Schedule调度系统

Schedule

react有一套基于Fiber架构的调度系统,这套调度系统的基本功能包括:

  • 1 更新具有不同的优先级

  • 2 一次根因可能涉及多个组件的render,这些render可能分配到多个宏任务中执行(即时间切片)

  • 3 搞优先级更新打断低优先级更新

实现第一版的调度系统,如图:

preview

(图借于魔术师卡颂)

可以看到是同步的,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,  //1UserBlockingPriority, //2 NormalPriority, // 3LowPriority //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;  //当前工作wrokfunction 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变化,代表这是新的workschedule(); //继续调度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变化,代表这是新的workschedule(); //继续调度const newCallback = curCallback; //最新调度的任务//如果上一个调度的任务跟最新调度任务一样if (newCallback && prevCallback === newCallback) {// callback没变,代表是同一个work,只不过时间切片时间用尽(5ms)// 返回的函数会被Scheduler继续调用return perform.bind(null, work);}
}

借用两张图(来自卡颂):

调度系统的实现原理

img

img

总结:
  • Schedule定义了几个优先级概念,值越低优先级越高,提供了scheduleCallback函数用来已不同的优先级注册函数。每次执行Schedul会从当前所有的任务中,挑选出优先级最高的任务,在开始调度前,如果有正在执行的调度,并且正在执行的调度的优先级比较低的时候,必须调用cancelCallback中断当前的任务,开始调度高优先级的任务。

  • 调度执行对应任务的perform方法,如果该任务已经超时,或者属于ImmediatePritority(同步执行优先级),则不管当前桢限制,必须立马执行他。否则该任务会根据当前桢的剩余时间来决定是否继续执行。

  • 当任务没执行完成的时候,他不会从当前堆中去除,如果没有更高优先级任务的时候,如果是因为时间切片限制,即当前桢剩余时间不够,那么下次调度还是获取到当前的任务继续执行。当任务执行完毕后,就会从当前堆中去掉,下次调度就取下一个优先级较低的任务执行

  • 而可中断的异步更新,就是在当当前任务执行的时候,由于当前桢时间不够,被中断了,然后继续调度的时候,schedule发现了更高优先级的任务,此时,下次执行perfrom的任务就是这个高优先级的任务了。上一个未完成的任务被中断,等到高优先级的任务完成之后才会继续往下完成。

看卡老师的demo地址:

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
本文仅学习笔记使用。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部