2

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

 2 years ago
source link: https://segmentfault.com/a/1190000041126142
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

发布于 今天 02:16
English

大家好,我卡颂。

想必大家都知道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是独立的包。所以可以用他改造我们的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

高优先级打断低优先级的例子

举例来看一个高优先级打断低优先级的例子:

  1. 插入一个低优先级work,属性如下
const work1 = {
  count: 100,
  priority: LowPriority
}
  1. 经历schedule(调度),perform(执行),在执行了80次工作时,突然插入一个高优先级work,此时:
const work1 = {
  // work1已经执行了80次工作,还差20次执行完
  count: 20,
  priority: LowPriority
}
// 新插入的高优先级work
const work2 = {
  count: 100,
  priority: ImmediatePriority
}
  1. work1工作中断,继续schedule。由于work2优先级更高,会进入work2对应perform,执行100次工作
  2. work2执行完后,继续schedule,执行work1剩余的20次工作

在这个例子中,我们需要区分2个打断的概念:

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

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

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

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

调度系统的实现原理

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

对照流程图来看:

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

  • schedule
  • perform

这里是完整Demo地址


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK