一文搞懂react的Schedule调度系统

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞懂react的Schedule调度系统相关的知识,希望对你有一定的参考价值。

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地址:

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

以上是关于一文搞懂react的Schedule调度系统的主要内容,如果未能解决你的问题,请参考以下文章

一文搞懂springboot定时任务

一文带你搞懂React生命周期

别告诉我你连线程池都不会用~ 一文搞懂线程池

CAS和AQS一文搞懂

第1945期彻底搞懂React源码调度原理(Concurrent模式)

一文搞懂前端路由的原理(VueReactAngular)