乐趣区

关于javascript:100行代码实现React核心调度功能

大家好,我卡颂。

想必大家都晓得 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 地址。

退出移动版