react - 从更新流程看 react 中的各种优先级

从更新流程看 react 中的各种优先级

react fiber 架构带来了时间切片。在时间切片的支持下,高优先级任务可以打断低优先级任务。react 最开使用 ExpirationTime 作为任务优先级的衡量,后续将 ExpirationTime 切换成了 Lane 优先级。关于 ExpirationTime 以及 lane 前世今生,可以参考 关于 react 为什么要从 ExpirationTime 切换到 lane 的一次考古。可以说 Lane 优先级比 ExpirationTime 更为强大。实际上,React 中除了 Lane 优先级之外还有其他的几种优先级。这篇文章将说明这几种优先级在整个更新流程中的作用。

基本概念

react 更新流程

在 react 中通过 class 组件或者 state hook 来改变组件的状态来触发页面的更新。每当我们调用一次 setState,react 会产生一个 update。并且 react 会根据该 update 产生一个渲染任务(也有可能复用之前的任务),这里简称任务。在这个任务中会进行 fiber 的 diff 并将最终结果 patch 到真实 dom 上。而创建的任务也并非立即执行, 它会被放入调度器队列,经过调度器调度执行。因此在实际的开发过程中,从事件产生到 dom 改变,往往会经过:

  1. 用户事件(例如点击,拖拽)产生,执行回调
  2. 回调中调用 setState 改变状态,产生 update
  3. react 创建渲染任务
  4. 调度器调度执行

在整个过程中都涉及到各种优先级:

  1. 用户事件产生: 根据不同的事件类型执行回调,并指定不同的事件优先级
  2. 状态改变,产生 update: 根据产生该 update 的事件优先级,计算 update 优先级
  3. react 创建渲染任务: 根据本次 update 以及目前尚未处理完成的工作计算 渲染优先级,并根据渲染优先级计算调度优先级
  4. 调度器调度执行: 根据挂载到 fiber 上的 update 优先级以及渲染优先级来控制需要跳过的 fiber 以及 update

Lane 优先级

在更新流程中提到的的事件优先级,update 优先级以及渲染优先级本质都是 Lane 优先级。Lane 优先级的基本类型 Lane 在代码中使用仅有 1 位为 1,其他位都为 0 的 31 位 2 进制数表示。其中 1 的位置约靠近左边,则表示该 lane 优先级越高。因此一个 lane 的值越小则对应的优先级反而越高。部分优先级的定义如下,SyncLane 的优先级就高于 InputContinuousHydrationLane。

export const TotalLanes = 31;

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;

const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;
复制代码

在 react 中还有一个概念称为 Lanes,表示多个 Lane 的合集。其可以通过将多个 Lane 优先级求或运算来得到。在很多地方会通过 lanes & lane !== NoLane 的方法来判断某个 lane 是否属于某个 lanes。其中 NoLane 的值为 0。

事件优先级

开发者在 react 回调中所接触到的事件都是 react 的中的合成事件。当某个事件触发后,react 在调用回调函数之前会设置一个事件优先级。例如点击事件属于 DiscreteEventPriority,其优先级高于拖拽事件的 ContinuousEventPriority 优先级。具体的事件类型和优先级的映射关系可以参考事件优先级。这里列出 react 中定义了以下几种事件优先:

export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;
复制代码

可以看出每个事件优先级都是等于一个 Lane 优先级。这里为什么不直接使用 Lane 优先级呢?我估计是为了解耦,例如以后 React 想将 Lane 优先级更换为其他优先级,那只需要更改事件优先级的定义即可。事件优先级会影响该事件回调函数中产生的 update 的优先级。

update 优先级

每当有状态改变,react 会产生一个 update。多个 update 还会形成环状链表,详细的处理可以参考React Fiber - updateQueue 原理分析。产生的每个 update 具有一个优先级,称为 update 优先级当 update 产生后。此外这个 update 优先级还有以下作用:

  1. react 会将该 update 优先级递归添加到父 fiber 的 childLanes 中。因此如果每个 fiber 产生了 update,则其父 fiber 的 childLanes 优先级一定大于等于该 update 优先级
  2. react 会将该 update 的优先级添加到对应 fiber 的优先级中,也就是 fiber.lanes
  3. react 会将该 update 优先级添加到 root 节点的 pendingLanes 中,表示该优先级对应的 update 有尚未完成的工作

update 优先级的计算方法有以下几种:

  1. 如果该 update 在合成事件回调中产生,则该 update 优先级等于合成事件的事件优先级
  2. 如果该 update 在 setTimeout,setInterval, useEffect 等非合成事件回调产生,则 update 优先级等于 DefaultLane
  3. 如果该 update 在 React.startTransition 回调中产生,那么该 update 优先级会从 TransitionLanes 选取一个产生。

调度优先级

当 update 产生后,会根据 update 优先级以及当前未完成的工作进行计算,得出本次任务的渲染优先级。并根据渲染优先级计算出调度优先级。这里需要注意的是,由于调度器实际上是可以脱离 react 存在的模块,因此调度优先级和update/渲染优先级/事件优先级不同并不是 lane 优先级。需要通过以下的逻辑进行转换:

switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
复制代码

每次一个任务结束,调度器会选出优先级最高的任务进行调度。

渲染优先级

在 react 渲染任务中,react 会从 root 节点开始对整棵 fiber 树进行遍历。但是在一次更新中,往往只有很少的组件发生了状态改变。又或者说,这个节点的状态改变了,但是其优先级不够。这两种情况下都可以跳过一些节点的状态更新或 diff 计算。因此 react 并不会遍历整颗树,而是根据根据 props 以及状态的改变进行优化,跳过一部分的 fiber。在 react 中有以下逻辑:

  1. fiber 状态改变,优先级足够。则计算最新状态,并 diff 子节点。
  2. fiber 状态未改变或优先级不够,则直接拷贝子节点(如果 props 未发生改变)。

因此 react 会比较 fiber 上挂载的 update 优先级与渲染优先级是否有重合的部分。如果有,则说明挂载到 fiber 的 update 至少有一个是满足优先级。此时会进行最新状态的计算。计算的过程种还会比较某个 update 的优先级是否在渲染优先级中。如果不在,则说明该 update 优先级不够,会直接跳过。如果没有重回的部分则命中逻辑 2,不会计算最新状态,也不会 diff 子节点。

这里举两个例子,例如本次渲染优先级 renderLanes 为 SyncLane,而某个 fiber 上有两个 update,分别为 SyncLane 以及 InputContinuousHydrationLane,经过或运算得到挂载在 fiber 上的 lanes:

fiber.lanes = SyncLane|InputContinuousHydrationLane = 0b0000000000000000000000000000011
复制代码

此时,由于 (renderLanes & fiber.lanes) !== NoLanes,因此该 fiber 状态改变且优先级满足,会计算最新状态。而两个 update 中的 InputContinuousHydrationLane 对应的 update 会直接被跳过,因为其优先级未达到本次的渲染优先级。而此时如果某个 fiber 只有一个 update 且为 InputContinuousHydrationLane,由于 renderLanes & InputContinuousHydrationLane === NoLane,那么这个 fiber 由于优先级不够,本次渲染任务不会计算最新状态。

总结

最后总结各优先级的功能:

  • 事件优先级: 根据不同事件类型产生,控制该事件中产生的 update 的优先级。用 Lane 优先级表示。
  • update 优先级: 某个 update 的优先级,影响渲染优先级。属于 Lane 优先级。
  • 渲染优先级: 控制调度优先级。并且根据该优先决定哪些 fiber 需要 diff,以及哪些 update 需要计算。属于 Lane 优先级。
  • 调度优先级: 根据渲染优先级产生,决定本次任务被调度的优先级。属于调度优先级。

猜你喜欢

转载自juejin.im/post/7096853196131270664
今日推荐