100行代码实现React核心调度功能

共 4800字,需浏览 10分钟

 ·

2021-12-19 14:15

作者:卡颂

简介:《React技术揭秘》作者

来源:SegmentFault  思否社区


大家好,我卡颂。


想必大家都知道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更新流程的类比。


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



包括三步:


  1. workList队列(用于保存所有work)插入work

  2. schedule方法从workList中取出work,传递给perform

  3. 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链接:https://github.com/facebook/react/tree/main/packages/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

expirationstartTime为当前开始时间,不同优先级的timeout不同。

比如,ImmediatePrioritytimeout为-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个打断的概念:

  1. 在步骤3中,work1执行的工作被打断。这是微观角度的打断

  2. 由于work1被打断,所以继续schedule。下一个执行工作的是更高优的work2work2的到来导致work1被打断,这是宏观角度的打断


之所以要区分宏/微观,是因为微观的打断不一定意味着宏观的打断

比如:work1由于时间切片用尽,被打断。没有其他更高优的work与他竞争schedule的话,下一次perform还是work1

这种情况下微观下多次打断,但是宏观来看,还是同一个work在执行。这就是时间切片的原理。

调度系统的实现原理



以下是调度系统的完整实现原理:


对照流程图来看:


总结



本文是React调度系统的简易实现,主要包括两个阶段:

  • schedule
  • perform

完整Demo地址:https://codesandbox.io/s/xenodochial-alex-db74g?file=/src/index.ts




点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -

浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报