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

开发 前端
想必大家都知道React有一套基于Fiber架构的调度系统,本文会用100行代码实现这套调度系统,让你快速了解React的调度原理。

[[440697]]

大家好,我卡颂。

想必大家都知道React有一套基于Fiber架构的调度系统。这套调度系统的基本功能包括:

  • 更新有不同优先级
  • 一次更新可能涉及多个组件的render,这些render可能分配到多个宏任务中执行(即时间切片)
  • 高优先级更新会打断进行中的低优先级更新

本文会用100行代码实现这套调度系统,让你快速了解React的调度原理。

我知道你不喜欢看大段的代码,所以本文会以图+代码片段的形式讲解。

文末有完整的在线Demo,你可以自己上手玩玩。

开整!

准备工作

我们用work这一数据结构代表一份工作,work.count代表这份工作要重复做某件事的次数。

在Demo中要重复做的事是“执行insertItem方法,向页面插入”:

  1. const insertItem = (content: string) => { 
  2.   const ele = document.createElement('span'); 
  3.   ele.innerText = `${content}`; 
  4.   contentBox.appendChild(ele); 
  5. }; 

所以,对于如下work:

  1. const work1 = { 
  2.   count: 100 

代表:执行100次insertItem向页面插入100个。

work可以类比React的一次更新,work.count类比这次更新要render的组件数量。所以Demo是对React更新流程的类比

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

包括三步:

  1. 向workList队列(用于保存所有work)插入work
  2. schedule方法从workList中取出work,传递给perform
  3. perform方法执行完work的所有工作后重复步骤2

代码如下:

  1. // 保存所有work的队列 
  2. const workList: work[] = []; 
  3.  
  4. // 调度 
  5. function schedule() { 
  6.   // 从队列尾取一个work 
  7.   const curWork = workList.pop(); 
  8.    
  9.   if (curWork) { 
  10.     perform(curWork); 
  11.   } 
  12.  
  13. // 执行 
  14. function perform(workWork) { 
  15.   while (work.count) { 
  16.     work.count--; 
  17.     insertItem(); 
  18.   } 
  19.   schedule(); 

为按钮绑定点击交互,最基本的调度系统就完成了:

  1. button.onclick = () => { 
  2.   workList.unshift({ 
  3.     count: 100 
  4.   }) 
  5.   schedule(); 

点击button就能插入100个。

用React类比就是:点击button,触发同步更新,100个组件render

接下来我们将其改造成异步的。

Scheduler

React内部使用Scheduler完成异步调度。

Scheduler是独立的包。所以可以用他改造我们的Demo。

Scheduler预置了5种优先级,从上往下优先级降低:

  • ImmediatePriority,最高的同步优先级
  • UserBlockingPriority
  • NormalPriority
  • LowPriority
  • IdlePriority,最低优先级

scheduleCallback方法接收优先级与回调函数fn,用于调度fn:

  1. // 将回调函数fn以LowPriority优先级调度 
  2. scheduleCallback(LowPriority, fn) 

在Scheduler内部,执行scheduleCallback后会生成task这一数据结构:

  1. const task1 = { 
  2.   expiration: startTime + timeout, 
  3.   callback: fn 

task1.expiration代表task1的过期时间,Scheduler会优先执行过期的task.callback。

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

比如,ImmediatePriority的timeout为-1,由于:

  1. startTime - 1 < startTime 

所以ImmediatePriority会立刻过期,callback立刻执行。

而IdlePriority对应timeout为1073741823(最大的31位带符号整型),其callback需要非常长时间才会执行。

callback会在新的宏任务中执行,这就是Scheduler调度的原理。

用Scheduler改造Demo

改造后的流程如图:

改造前,work直接从workList队列尾取出:

  1. // 改造前 
  2. const curWork = workList.pop(); 

改造后,work可以拥有不同优先级,通过priority字段表示。

比如,如下work代表「以NormalPriority优先级插入100个」:

  1. const work1 = { 
  2.   count: 100, 
  3.   priority: NormalPriority 

改造后每次都使用最高优先级的work:

  1. // 改造后 
  2. // 对workList排序后取priority值最小的(值越小,优先级越高) 
  3. const curWork = workList.sort((w1, w2) => { 
  4.    return w1.priority - w2.priority; 
  5. })[0]; 

改造后流程的变化

由流程图可知,Scheduler不再直接执行perform,而是通过执行scheduleCallback调度perform.bind(null, work)。

即,满足一定条件的情况下,生成新task:

  1. const someTask = { 
  2.   callback: perform.bind(nullwork), 
  3.   expiration: xxx 

同时,work的工作也是可中断的。在改造前,perform会同步执行完work中的所有工作:

  1. while (work.count) { 
  2.   work.count--; 
  3.   insertItem(); 

改造后,work的执行流程随时可能中断:

  1. while (!needYield() && work.count) { 
  2.   work.count--; 
  3.   insertItem(); 

needYield方法的实现(何时会中断)请参考文末在线Demo

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

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

插入一个低优先级work,属性如下

  1. const work1 = { 
  2.   count: 100, 
  3.   priority: LowPriority 

经历schedule(调度),perform(执行),在执行了80次工作时,突然插入一个高优先级work,此时:

  1. const work1 = { 
  2.   // work1已经执行了80次工作,还差20次执行完 
  3.   count: 20, 
  4.   priority: LowPriority 
  5. // 新插入的高优先级work 
  6. const work2 = { 
  7.   count: 100, 
  8.   priority: ImmediatePriority 

work1工作中断,继续schedule。由于work2优先级更高,会进入work2对应perform,执行100次工作

work2执行完后,继续schedule,执行work1剩余的20次工作

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

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

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

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

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

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

调度系统的实现原理

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

对照流程图来看:

总结

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

  • schedule
  • perform

如果你对代码的具体实现感兴趣,下面是完整Demo地址。

参考资料

[1]Scheduler:

https://github.com/facebook/react/tree/main/packages/scheduler

[2]完整Demo地址:

https://codesandbox.io/s/xenodochial-alex-db74g?file=/src/index.ts

 

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2022-04-15 08:07:21

ReactDiff算法

2023-07-03 07:51:47

2021-12-26 12:10:21

React组件前端

2022-02-08 12:30:30

React事件系统React事件系统

2021-10-27 06:55:18

ReacFiber架构

2023-11-27 07:10:06

日志中间件

2020-08-21 13:40:17

Python代码人体肤色

2015-02-09 10:43:00

JavaScript

2022-03-14 09:57:30

Python代码

2017-03-28 21:03:35

代码React.js

2023-05-04 07:34:37

Rust代码CPU

2022-07-07 15:50:19

Python开发功能

2020-04-10 12:25:28

Python爬虫代码

2017-02-08 14:16:17

C代码终端

2018-02-08 16:45:22

前端JS粘贴板

2022-04-12 08:09:22

Nodejs前端面试题

2020-03-26 12:38:15

代码节点数据

2018-01-10 22:19:44

2019-11-15 15:50:41

JS代码React前端

2022-12-06 08:30:06

SchedulerReact
点赞
收藏

51CTO技术栈公众号