进阶学习之React Fiber

115 阅读21分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

一 前置知识

我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。大多数设备的屏幕刷新率为1s 60次,当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。
1000ms / 60 = 16.7ms

浏览器是多进程的。

  • 浏览器主进程
  • GPU进程
  • 第三方插件进程
  • 浏览器渲染进程

浏览器渲染进程又是多线程的。

  • GUI渲染线程
  • JS引擎线程
  • 定时触发线程
  • 事件触发线程
  • 异步http请求线程

我们知道,JS是一个单线程的语言。浏览器中UI渲染线程和js线程互斥,执行js时无法进行UI渲染,长时间执行js导致UI渲染线程长时间挂起,页面就会卡顿甚至直接卡死。
完整的一帧都做了什么事情。
image.png

  • 1 用户进行输入或者执行点击等操作
  • 2 执行事件的回调
  • 3 处理开始帧,例如resize、scroll等
  • 4 在绘制之前执行requestAnimationFrame(请求动画帧)
  • 5 Layout 计算布局,位置信息
  • 6 重绘,根据尺寸和位置进行元素的内容填充
  • 7 前六个阶段处理完成之后,可能用时到不了16.7ms,仅仅只用了4ms,这时候会有一个空闲阶段。这时候是可以去执行requestIdleCallback里的注册任务。(下面再去讲这个回调函数,它是React Fiber 的实现基础)

二 React

JSX 是如何转换的

const element = (
  <div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
  </div>
)

babel 跳转

React 是如何工作的

import React from "react";
import ReactDOM from "react-dom";

const element = (
  <div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
  </div>
);

console.log(element);

ReactDOM.render(element, document.getElementById("root"));

下面的就是我们常说的虚拟DOM
image.png
我们简化一下

const element = {
  type: 'div',
  props: {
    children: [
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      },
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      },
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      }
    ]
  }
}

一个React组件的渲染主要经历两个阶段
调度阶段:用新的数据生成一个新的树,通过 diff 算法遍历旧的树,快速找出需要更新的元素,放到更新队列中,得到新的更新队列。
渲染阶段:遍历更新队列,通过调用宿主环境api,实际更新渲染对应的元素。

React 15

在 react16 引入 Fiber 架构之前,react使用的架构是《栈调和》(stack reconciler),react 会采用递归对比虚拟DOM树,通过 diff算法 找出需要变动的节点,然后同步更新它们,这个过程 react 称为reconcilation(协调)。也就是通过例如react-dom类库使虚拟dom与真实的dom同步,这个过程就是协调。
在reconcilation 期间,react 会一直占用浏览器资源,会导致用户触发的事件得不到响应。react 15 是如何将 JSX 渲染到页面上的。通过深度优先遍历,遍历自上到下进行遍历。
image.png
遍历的顺序 A1 -> B1 -> C1 -> C2 -> B2 -> C3 -> C4

这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。如果递归花了100ms,则这100ms浏览器是无法响应的,而我们感觉流畅的最大时间也就是16.7ms,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。
React 15 vs React 16

核心对比

StackFiber
版本15.x16.x
数据结构数组(树)链表
任务调度不能暂停可暂停、中断、恢复

抽象对比

stack
image.png
fiber
image.png

效果对比

谢尔宾斯基三角形:百度百科

  1. 在每一帧渲染之前(通过requestAnimationFrame方法)给最外层的div设置一个缩放的transform,也就是让整个div开启一个不停变大缩小的动画。
  2. 设置一个定时器,每过1秒钟就改变一次组件的状态,也就是执行一次setState,并且demo中的所有子组件都会受到影响并render一次。

stack 掉帧 claudiopro.github.io/react-fiber…
fiber 不掉帧 claudiopro.github.io/react-fiber…

三 React Fiber

为了解决react15中组件render过程耗时过多,或者参与调和阶段的虚拟DOM节点过多的问题,那么react团队在react16版本提出了fiber架构和scheduler任务调度。fiber架构的目的是【能够独立执行每个虚拟DOM的调和阶段】,而不是每次执行整个虚拟DOM树的调和阶段。

什么是 React Fiber

Fiber的英文含义叫做"纤维",计算机领域中有两个大家很熟悉的概念:进程(Process)和线程(Thread)意思就是指的比Thread更细的概念,也就是比线程控制的更加精密的并发处理结构。
React Fiber并不是所谓的纤程(微线程)那种概念。而是一种基于浏览器的单线程调度算法。背后其实是基于 **requestIdleCallback **这个API的原理(自己实现的一套调度机制),Fiber是一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。

四 React Fiber 实现原理

requestIdleCallback

react fiber 中使用了这个API的原理,requesetIdleCallback。这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

由于requestIdleCallback利用的是帧的空闲时间, 所以有可能出现浏览器一直处于繁忙状态, 导致回调一直无法执行, 那这时候就需要在调用requestIdleCallback的时候传递第二个配置参数timeout了.

const workLoop = (deadline) => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
  // 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
  // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    taskQueue.length > 0
  ) {
    performUnitWork();
  }
  
  // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
  if (taskQueue.length > 0) {
    requestIdleCallback(workLoop, { timeout: 1000 });
  }
};
requestIdleCallback(workLoop, { timeout: 1000 });

拓展资料:requestIdleCallback

一个执行单元

Fiber 可以理解为一个执行的单元,如下图所示,浏览器在每一帧都有一个空闲时间,react正是利用这个空闲时间去执行分片后优先级较高的任务。一旦空闲时间结束之后把执行权再交给浏览器。
React 和浏览器配合调度关系图

每一个fiber节点都是一个 fiber。一个 fiber 包含child、sibling、return。

  • return:指向父节点,若没有父fiber则为 null
  • child:指向第一个子 fiber,若没有任何子 fiber 则为 null
  • sibling:指向下一个兄弟 fiber,若没有下一个兄弟 fiber 则为 null
<App>
  <div />
  <input />
  <List>
    <div />
    <div />
  </List>
</App>

image.png
记住一个规则:优先儿子,再兄弟。

一种数据结构

源码:react fiber 数据结构

FiberNode:源码

// packages/react-reconciler/src/ReactInternalTypes.js

export type Fiber = {|
  // 作为静态数据结构,存储节点 dom 相关信息
  tag: WorkTag, // 组件的类型,取决于 react 的元素类型
  key: null | string,
  elementType: any, // 元素类型
  type: any, // 定义与此fiber关联的功能或类。对于组件,它指向构造函数;对于DOM元素,它指定HTML tag
  stateNode: any, // 真实 dom 节点
  
  // fiber 链表树相关, 主要
  return: Fiber | null, // 父 fiber
  child: Fiber | null, // 第一个子 fiber
  sibling: Fiber | null, // 下一个兄弟 fiber
  index: number, // 在父 fiber 下面的子 fiber 中的下标
  
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,
    
  // 工作单元,用于计算 state 和 props 渲染
  pendingProps: any, // 本次渲染需要使用的 props
  memoizedProps: any, // 上次渲染使用的 props
  updateQueue: mixed, // 用于状态更新、回调函数、DOM更新的队列
  memoizedState: any, // 上次渲染后的 state 状态
  dependencies: Dependencies | null, // contexts、events 等依赖
  
  mode: TypeOfMode,
  
  // 副作用相关
  flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态
  subtreeFlags: Flags, // 当前子树的副作用状态
  deletions: Array<Fiber> | null, // 要删除的子 fiber
  nextEffect: Fiber | null, // 下一个有副作用的 fiber
  firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
  lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber 
  
  // 优先级相关
  lanes: Lanes,
  childLanes: Lanes,
  
  alternate: Fiber | null, // 指向 workInProgress fiber 树中对应的节点
  
  actualDuration?: number,
  actualStartTime?: number,
  selfBaseDuration?: number,
  treeBaseDuration?: number,
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,
  _debugHookTypes?: Array<HookType> | null,
|};

WorkTag:源码
组件的类型,取决于 react 的元素类型

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

双缓存Fiber树

什么是 双缓存?
当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。
如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
这种在内存中构建并直接替换的技术叫做双缓存。
react fiber 就是使用双缓存完成Fiber树的构建与替换。DOM 的创建于更新。

那么 react fiber 是如果进行双缓存 fiber 树的?

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。
在首次执行ReactDOM.render的时候,会创建一个 fiberRootNode,他是整个应用的根。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

**mount **
首次执行 ReactDOM.render 的时候,页面还未挂载 DOM。current tree是空的。

update
workInProgress fiber的创建可以复用current Fiber树对应的节点数据。

effect list

与react15不同的是,fiber采用链表树的形式实现的,我们刚刚了解了在render阶段,react 采用深度优先遍历对 fiber 树进行遍历。把每个有副作用的 fiber 筛选出来,构建一个只带有副作用的effect list链表。
这个链表包括 firstEffect、nextEffect、lastEffect。

Fiber 调和的过程

fiber reconciler 在执行的过程中,从根节点开始渲染和调度的过程可以分为两个阶段:render,commit

  • render 阶段,生成 fiber 树,得出需要更新的节点信息,这一步是渐进的过程,是可以被中途打断的。
  • commit 阶段,讲需要更新的节点一次性批量更新,这个过程是不可以被打断的。

render

在 render 阶段时,这个过程是实际上在内存中构建的,也称为(workInProgress树)。react 会采用深度优先遍历,对 fiber 树进行向下遍历再向上回溯到root的过程(下图就是向下遍历以及向上回溯的过程),其中每个Virtual DOM 都可以表示为一个 fiber。下图的 div和input等都是一个 fiber。

agh1i-dhcs9 2.gif
事实上,react在构建这个workInProgress树的过程中,会经历两个阶段:beginWork和completeWork。
beginWork:组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段。
p3-juejin.byteimg.com/tos-cn-i-k3…
在 beginWork阶段,更新节点并返回子树,进入beginWork后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和diff之后生成新的Fiber,然后在新的fiber上标记flags(effectTag),最后return它的子节点,以便继续针对子节点进行beginWork。若它没有子节点,则返回null,这样说明这个节点是末端节点,可以进行向上回溯,进入completeWork阶段。

看源码部分

beginWork阶段的整体工作是去更新节点,并返回子树,但真正的beginWork函数只是节点更新的入口,不会直接进行更新操作。作为入口,它的职责很明显,拦截无需更新的节点。同时,它还会将context信息入到栈中(beginWork入栈,completeWork出栈)

两个地方需要注意:

如何区分初始化还是更新?
判断 current 是否存在,我们知道,调度的过程是有两棵树的存在,展示在屏幕上的current Tree和正在后台基于current树构建的 workInProgress Tree。
如果是首次渲染,对具体的workInProgress节点来说,它是没有current节点的,如果是在更新过程,由于current节点已经在首次渲染时产生了,所以workInProgress节点有对应的current节点存在。
最终会根据节点是首次渲染还是更新来决定是创建fiber还是diff fiber。只不过更新时,如果节点的优先级不够会直接复用已有节点,即走跳出(bailout)的逻辑,而不是去走下面的更新逻辑。

如何复用节点?

通过 bailoutOnAlreadyFinishedWork 函数返回。

beginWork它的返回值有两种情况:

  • 返回当前节点的子节点,然后会以该子节点作为下一个工作单元继续beginWork,不断往下生成fiber节点,构建workInProgress树。
  • 返回null,当前fiber子树的遍历就此终止,从当前fiber节点开始往回进行completeWork。

复用的节点返回值仍然会有两种情况

  • 返回当前节点的子节点,前置条件是当前节点的子节点有更新,此时当前节点未经处理,是可以直接复用的,复用的过程就是复制一份current节点的子节点,并把它return出去。
  • 返回null,前提是当前子节点没有更新,当前子树的遍历过程就此终止。开始completeWork。

总结:beginWork的主要功能就是处理当前遍历到的fiber,经过一番处理之后返回它的子fiber,一个一个地往外吐出fiber节点,那么workInProgress树也就会被一点一点地构建出来。
image.png

completeWork:effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。

completeWork 发生在 beginWork之后,详细点说是发生在 Diff 之后。这时候workInProgress节点是经过 diff 算法调和过的。这时候fiber基本形态已经确定了。

  • fiber 形态变了,但是原生DOM(HostComponent和HostTest)对应的DOM还没有更新变化。
  • 经过diff生成的workInProgress节点有了的flag(effectTag)。

workInProgress节点的completeWork阶段主要做的事情再来回顾一下:

  • 真实DOM节点的创建以及挂载
  • DOM属性的处理
  • effectList的收集
  • 错误处理

commit

commit阶段主要做的是,根据 fiber 的effect list的 effectTag 去更新视图。(新增、更新、删除)。源码在此

render 阶段结束之后,意味着内存中构建的 workInProgress 树所有的更新工作都已经完成,包括树中的 fiber 节点的更新、diff、effectTag的标记,effectList的收集。
和current树相比,它们的结构上固然存在区别,变化的fiber节点也存在于workInProgress树,但要将这些节点应用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保证了只针对有变化的节点做工作。
所以循环effectList链表去将有更新的fiber节点应用到页面上是commit阶段的主要工作。

入口函数是 **commitRoot****, **会给调度器说,要以立即执行的优先级去调度 commit 阶段的工作。

commitRoot 又分为三个阶段

  • before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
  • mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
  • layout:在DOM操作完成后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect

五 scheduler

如果「组件 Render 过程耗时」或「参与调和阶段的虚拟 DOM 节点很多」时,那么一次性完成所有组件的调和阶段就会花费较长时间。<br />为了避免长时间执行调和阶段而引起页面卡顿,[React](https://so.csdn.net/so/search?q=React&spm=1001.2101.3001.7020) 团队提出了 Fiber 架构和 Scheduler 任务调度。Fiber 架构的目的是「能独立执行每个虚拟 DOM 的调和阶段」,而不是每次执行整个虚拟 DOM 树的调和阶段。<br />scheduler 的主要功能是时间分片,每隔一段时间就把主线程还给浏览器,避免长时间占用主线程。scheduler是一个独立的包,抛开react来讲,它可以作为第三方库使用,只是react使用了scheduler进行任务调度。<br />你只需要将 任务和 任务的优先级给到scheduler,他就可以帮你管理任务了,可以安排任务的执行顺序。**commitRoot 的例子**。

scheduler的两个行为

  • 对于多个任务,会执行优先级高的那个。
  • 对于单个任务,会有节制的执行。线程只有一个,不会一直占用线程执行任务,而是执行一会,中断一下。如此往复。

从scheduler的行为可以分析,多个任务直接是有一个任务优先级的概念。对与单个任务,是有时间片段(任务中断机制)。单个任务在一帧当中最大的执行时间,一旦执行时间超过时间片,则会被打断,有节制的执行任务。这样可以保证页面不会因为任务连续执行的时间过长而产生卡顿。

可以分析,scheduler的两大概念,任务队列管理时间片下的任务中断恢复

任务队列管理

在Scheduler内部,把任务分成了两种:未过期的和已过期的,分别用两个队列存储,前者存到timerQueue中,后者存到taskQueue中。

如何区分任务是否过期?
用任务的开始时间(startTime)和当前时间(currentTime)作比较。

  • 开始时间大于当前时间,说明未过期,放到timerQueue;
  • 开始时间小于等于当前时间,说明已过期,放到taskQueue

任务入队两个队列之后要干嘛?

  • 如果放到了taskQueue,那么立即调度一个函数去循环taskQueue,挨个执行里面的任务。
  • 如果放到了timerQueue,那么说明它里面的任务都不会立即执行,那就等到了timerQueue里面排在第一个任务的开始时间,看这个任务是否过期,如果是,则把任务从timerQueue中拿出来放入taskQueue,调度一个函数去循环它,执行掉里面的任务;否则过一会继续检查这第一个任务是否过期。

任务队列管理相对于单个任务的执行,是宏观层面的概念,它利用任务的优先级去管理任务队列中的任务顺序,始终让最紧急的任务被优先处理。

时间片下的任务中断恢复

单个任务的中断以及恢复对应了Scheduler的单个任务执行控制这一行为。在循环taskQueue执行每一个任务时,如果某个任务执行时间过长,达到了时间片限制的时间,那么该任务必须中断,以便于让位给更重要的事情(如浏览器绘制)

这个任务中断实际上涉及到了两个角色:调度者、任务执行者(这两个角色实际上都在scheduler阶段)

image.png

调度的入口:scheduleCallback

调度者:requestHostCallback中进行实现,浏览器情况下是postMessage,非浏览器下是setTimeout

执行者:performWorkUntilDeadline,它的作用是按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续任务。performWorkUntilDeadline
内部实际是调用了scheduledHostCallback,最初的时候是被赋值为了flushWork,所以,flushWork是真正的执行者。

flushWork 返回的是workLoop,工作循环进行事件的执行。

故,任务的中断和恢复是在 workLoop中进行的。看下workLoop干了什么事情。

优先级管理

react 中的优先级

  • 事件优先级:按照用户事件的交互紧急程度,划分的优先级,用户输入一个字符。。。

离散事件(DiscreteEvent):click、keydown、focusin等,这些事件的触发不是连续的,优先级为0。
用户阻塞事件(UserBlockingEvent):drag、scroll、mouseover等,特点是连续触发,阻塞渲染,优先级为1。
连续事件(ContinuousEvent):canplay、error、audio标签的timeupdate和canplay,优先级最高,为2。

  • 更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)

  • 任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级

  • 调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级

前三者属于React的优先级机制,第四个属于Scheduler的优先级机制,Scheduler内部有自己的优先级机制,虽然与React有所区别,但等级的划分基本一致。下面我们从事件优先级开始说起。

三者的关系:

  • LanePrioritySchedulerPriority从命名上看, 它们代表的是优先级
  • ReactPriorityLevel从命名上看, 它代表的是等级而不是优先级, 它用于衡量LanePrioritySchedulerPriority的等级.

[SchedulerWithReactIntegration中](github.com/facebook/re…), 可以互转SchedulerPriorit和 ReactPriorityLevel

// 把 SchedulerPriority 转换成 ReactPriorityLevel
export function getCurrentPriorityLevel(): ReactPriorityLevel {
  switch (Scheduler_getCurrentPriorityLevel()) {
    case Scheduler_ImmediatePriority:
      return ImmediatePriority;
    case Scheduler_UserBlockingPriority:
      return UserBlockingPriority;
    case Scheduler_NormalPriority:
      return NormalPriority;
    case Scheduler_LowPriority:
      return LowPriority;
    case Scheduler_IdlePriority:
      return IdlePriority;
    default:
      invariant(false, 'Unknown priority level.');
  }
}

// 把 ReactPriorityLevel 转换成 SchedulerPriority
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
  switch (reactPriorityLevel) {
    case ImmediatePriority:
      return Scheduler_ImmediatePriority;
    case UserBlockingPriority:
      return Scheduler_UserBlockingPriority;
    case NormalPriority:
      return Scheduler_NormalPriority;
    case LowPriority:
      return Scheduler_LowPriority;
    case IdlePriority:
      return Scheduler_IdlePriority;
    default:
      invariant(false, 'Unknown priority level.');
  }
}

[ReactFiberLane中](github.com/facebook/re…), 可以互转LanePriority和 ReactPriorityLevel

export function schedulerPriorityToLanePriority(
  schedulerPriorityLevel: ReactPriorityLevel,
): LanePriority {
  switch (schedulerPriorityLevel) {
    case ImmediateSchedulerPriority:
      return SyncLanePriority;
    // ... 省略部分代码
    default:
      return NoLanePriority;
  }
}

export function lanePriorityToSchedulerPriority(
  lanePriority: LanePriority,
): ReactPriorityLevel {
  switch (lanePriority) {
    case SyncLanePriority:
    case SyncBatchedLanePriority:
      return ImmediateSchedulerPriority;
    // ... 省略部分代码
    default:
      invariant(
        false,
        'Invalid update priority: %s. This is a bug in React.',
        lanePriority,
      );
  }
}

reconciler从输入到输出一共经历了 4 个阶段, 在每个阶段中都会涉及到与优先级相关的处理. 正是通过优先级的灵活运用, React实现了可中断渲染时间切片(time slicing),异步渲染(suspense)

react 团队自己实现的 requestIdleCallback

react 团队为什么不使用 requestIdleCallback,而是重写。
1 先看看 can i use,requestIdleCallback的兼容性其实很差。
Untitled.png
2 requestIdleCallback的定位是处理不重要、不紧急的任务。和React可能不太符(React渲染内容,并非是不紧急不重要)
3 requestIdleCallback的FPS只有20ms,正常情况下渲染一帧时长控制在16.67ms,该时间是高于页面流畅的需求的。

react 如何重写 requestIdleCallback 的

老版本:scheduler 直接使用 MessageChannel 实现。
新版本:优先使用 setImmediate,如果没有再去使用 MessageChannel。 传送门
Untitled.png
使用 MessageChannel 创建宏任务进行处理。

为什么不使用 setTimeout?

var count = 0
 
var startVal = +new Date()
console.log("start time", 0, 0)
function func() {
  setTimeout(() => {
    console.log("exec time", ++count, +new Date() - startVal)
    if (count === 50) {
      return
    }
    func()
  }, 0)
}
 
func()

点击查看,setTimeout会有一个5ms的浪费。

六 总结

  • 对大型复杂任务进行分片
  • 对任务进行优先级划分,优先调度高优先级的任务
  • 调度过程中,可以对任务进行挂起、恢复、终止等操作

react fiber 已经有一定的了解了,让我们一起来实现一个Mini-React吧。
学习react源码是极为漫长的事情,我们需要先大致了解一遍react执行的整个链路,然后再去逐个去学习每个知识点(createElement、fiber、hooks等)。