React 的优先级模型 - Lanes 模型

384 阅读16分钟

Lanes 模型

起因、需求

在react并发模式中,会有很多对优先级、批量更新进行判断的场景,比如:

  • 过期任务或者同步任务使用同步优先级(最高优)
  • 用户交互产生的更新(比如点击事件)使用高优先级
  • 网络请求产生的更新使用一般优先级
  • Suspense使用低优先级
  • 需要判断:某个 update 是否可以放在某次批量更新中进行更新?
  • 从批量更新中,删除属于某个优先级的任务

React需要设计一套满足如下需要的优先级机制:

  • 可以表示优先级的不同

  • 可能同时存在几个同优先级更新,所以还得能表示的概念

  • 方便进行优先级相关计算

lanes模型借鉴了同样的概念,使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级

React 对 lanes 模型的定义

Lane 和 Lanes

想象你身处赛车场。

不同的赛车疾驰在不同的赛道。内圈的赛道总长度更短,外圈更长。某几个临近的赛道的长度可以看作差不多长。

lanes模型借鉴了同样的概念,使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级

Lane 指代一条具体的赛道,通常表示单个更新的优先级,比如 setState 的优先级。lane 只有一位比特是 1 的 31 位二进制数

// react 中优先级最高的赛道: SyncLane
export const SyncLane: Lane =  0b0000000000000000000000000000001;

在前面起因、需求部分提到:可能同时存在几个同优先级更新,所以还得能表示的概念

React 用 Lanes 这个词来指代一批lane, lanes 是可能有多位比特是 1 的 31 位二进制数

Lanes 模型的第一个 mr是这样解释的lanes 的

In the new model, we have decoupled those two concepts. Groups of tasks are instead expressed not as relative numbers, but as bitmasks:

在新模型中,我们已经将这两个概念(任务优先级和任务分组)解耦。任务组不再表示为相对数字,而是表示为位掩码:

const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;

The type of the bitmask that represents a task is called a Lane. The type of the bitmask that represents a batch is called Lanes. (Note: these names are not final. I know using a plural is a bit confusing, but since the name appears all over the place, I wanted something that was short. But I'm open to suggestions.)

表示任务的位掩码类型称为 “车道”(Lane)。表示批次的位掩码类型称为 “车道组”(Lanes)。(注意:这些名称并非最终确定。我知道使用复数形式有点令人困惑,但由于这个名称在各处都有出现,我希望它简短一些。不过我也欢迎大家提出建议。)

React 中 定义的lane、lanes 常量:
Lane 表示单个更新的优先级,比如 setState 的优先级。lane 都只有一位比特是 1
Lanes 表示 “batch”(批次?)的优先级,比如 concurrent 模式中的批量更新。 lanes 会有多位比特位 1
// 从存储的角度来说,lane 和 lanes 都是 31 位二进制,没什么区别

// 0 代表没有优先级,没有实际意义, 一般用于辅助判断 lanes 之间是否有交集关系、优先级强弱。
// root.pendingLanes === NoLanes 就说明 root.pendingLanes 中没有要处理的车道。 暂时不必纠结
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

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

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

Lanes 相关的运算

lanes 实际上是31 位二进制数,所以经常会涉及到一些二进制的位运算。下面会放到具体例子中讲解

  • 判断某个 lanes 是否包含某个 lane
const isBelongBatch = taskLane & batchLanes !== NoLanes; 

NoLanes === 0,本身并不标识任何优先级,一般用于辅助判断 lanes 之间是否有交集关系、优先级强弱等等。比如root.pendingLanes === NoLanes 就说明 root.pendingLanes 中没有要处理的车道。

使用场景:

提交了一个低优先级的任务(比如 useTransication ),而该任务对应的 lane 不在当前 workinProgress正在处理的lanes里,没必要立即处理该低优先级任务,而是先记录在 fiber 上,后续调度对应lanes的任务时再做处理。

比如:

useTransication 对应的赛道: 0b000_0000_0000_0000_0000_0100_0000_0000 是 10 号赛道,

假设当前workinProgress 正在处理: 0b000_0000_0000_0000_0000_0000_0011_1111 是 0-5 号赛道

两者没有交集并且 10 号赛道的优先级低于 0-5 这一批赛道

因此 react 不会立即处理这个新的任务,而是先记录在 fiber 上,后续调度到包含 10 号赛道的批处理时再做处理。

  • 哪个lane的优先级高

lane 的数值越小,优先级越高。直接用<比较符就行。

const lane1= 0b00000001000
const lane2= 0b00001000000
 lane1<lane2 ===true 
所以 lane1 的优先级更高
  • 从某 lanes 中,剔除掉另一个 lanes 或 lane

在组件的生命周期里,更新的优先级或许会动态变化。比如,在用户进行交互的时候,某些更新的优先级会提高,而之前的一些低优先级更新可能就不再需要了。这时候就可以通过剔除特定的 lanes 来调整调度。

function removeLaneFromLanes(lanes, lane){
    return lanes & ~lane // ~表示按位取反,&表示按位与
} 

解释:假设有lanesA和 laneB的值如下
lanesA =           1111110010  // 
laneB  =           0001100001 // removeLaneFromBatch(lanesA,laneB) 的预期结果为 1110010010
即:lanesA和laneB中都为 1 的比特位在结果中为 0,其余比特位继承 batch

计算过程如下:
~laneA =           1110011110 // 对 lane 按位取反
lanesA & (~laneB) =1110010010 // 符合预期结果 ✅
  • 获取某 lanes 里优先级最高的 lane

这个问题可以转化为获取 lanes 这个比特串的「最低有效位所对应的 lane」,也即最右边的 1

const a              =  0b0000_0100_1010
const a_最低有效位lane =  0b0000_0000_0010
//                                    ^ 这里

那么怎么计算这个「最低有效位所对应的 lane」呢?

使用 & - 运算:

  1. 在计算机中,负数是以补码形式存储的。而补码计算方式是原码取反加 1。
  2. 当我们对 lanes 取负时,会得到其补码。 如 0b010100 的补码是 0b101100
  3. 然后将 lanes 与其补码进行按位与运算(&),最终结果会保留 lanes 中最右边的 1,其余位都变为 0。

例如,假设lanes为 0b010100(十进制 20),那么 -lanes 为 0b101100(补码形式),

lanes & -lanes 结果为 0b000100(十进制 4)。

react源码中用下面这个函数来获取某 lanes 中的最高优先级 lane

export function getHighestPriorityLane(lanes: Lanes): Lane {
  return lanes & -lanes;
}
  • 获取某 lanes 里优先级最低的 lane

类似地,有时也需要获取最低优先级,也即最左边的 1

const lanes              =  0b0000_0100_1010
const 优先级最低 lane      =  0b0000_0100_0000
//                                  ^ 这里

计算方式与获取最高优先级不太一样,直接看下面函数里的注释

 // 找到lanes中优先级最低的那一个lane
function getLowestPriorityLane(lanes: Lanes): Lane {
  // This finds the most significant non-zero bit.

   /** 
 *  @From MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
 * Math.clz32() 函数返回一个数字在转换成 32 无符号整形数字的二进制形式后,开头的 0 的个数
   * 比如 0b0000000000000000000000011100000, 开头的
 * 0 的个数是 24 个, 则
 * Math.clz32(0b0000000000000000000000011100000) 返回 24. 
 *        1 === 0b0000000000000000000000000000001
 * 1 << 24  =  0b0000001000000000000000000000000   // 1 << 24 代表把 1 对应的二进制数整体左移24 位
 * 
 * */ 

  const index = 31 - clz32(lanes);
  return index < 0 ? 
      NoLanes 
      : 
      1 << index;
}

在 React 中的实际应用

源码中涉及到 lanes 最多的**getNextLanes** getHighestPriorityLanes ****这两个函数

  • getNextLanes

该函数广泛用于react-reconciler,用于从root.pendingLanes中找出优先级最高的「预设车道」

预设车道为上文中列出来的那些,react 里定义的 lean/leans常量

 /**
* 该函数从root.pendingLanes中找出优先级最高的「预设车道」
* @param {*} root
* @param {*} wipLanes
* @returns
*/
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  // Early bailout if there's no pending work left.
  // 在没有剩余任务的时候,跳出更新
  const pendingLanes = root.pendingLanes;
  if (pendingLanes === NoLanes) {
    return_highestLanePriority = NoLanePriority;
    return NoLanes;
  }

  let nextLanes = NoLanes;
  let nextLanePriority = NoLanePriority;

  const expiredLanes = root.expiredLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;

  // Check if any work has expired.
  // 检查是否有更新已经过期
  if (expiredLanes !== NoLanes) {
    // 已经过期了,就需要把渲染优先级设置为同步,来让更新立即执行
    nextLanes = expiredLanes;
    nextLanePriority = return_highestLanePriority = SyncLanePriority;
  } else {
    // Do not work on any idle work until all the non-idle work has finished,
    // even if the work is suspended.
    // 即使具有优先级的任务被挂起,也不要处理空闲的任务,除非有优先级的任务都被处理完了

    // nonIdlePendingLanes 是所有需要处理的优先级。然后判断这些优先级
    // (nonIdlePendingLanes)是不是为空。
    //
    // 不为空的话,把被挂起任务的优先级踢出去,只剩下那些真正待处理的任务的优先级集合。
    // 然后从这些优先级里找出最紧急的return出去。如果已经将挂起任务优先级踢出了之后还是
    // 为空,那么就说明需要处理这些被挂起的任务了。将它们重启。pingedLanes是那些被挂起
    // 任务的优先级

    const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
    if (nonIdlePendingLanes !== NoLanes) {
      const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
      // nonIdleUnblockedLanes也就是未被阻塞的那些lanes,未被阻塞,那就应该去处理。
      // 它等于所有未闲置的lanes中除去被挂起的那些lanes。& ~ 相当于删除
      if (nonIdleUnblockedLanes !== NoLanes) {
        // nonIdleUnblockedLanes不为空,说明如果有任务需要被处理。
        // 那么从这些任务中挑出最重要的
        nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
        nextLanePriority = return_highestLanePriority;
      } else {
        // 如果目前没有任务需要被处理,就从正在那些被挂起的lanes中找到优先级最高的
        const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
        if (nonIdlePingedLanes !== NoLanes) {
          nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
          nextLanePriority = return_highestLanePriority;
        }
      }
    } else {
      // The only remaining work is Idle.
      // 剩下的任务是闲置的任务。unblockedLanes是闲置任务的lanes
      const unblockedLanes = pendingLanes & ~suspendedLanes;
      if (unblockedLanes !== NoLanes) {
        // 从这些未被阻塞的闲置任务中挑出最重要的
        nextLanes = getHighestPriorityLanes(unblockedLanes);
        nextLanePriority = return_highestLanePriority;
      } else {
        if (pingedLanes !== NoLanes) {
          // 找到被挂起的那些任务中优先级最高的
          nextLanes = getHighestPriorityLanes(pingedLanes);
          nextLanePriority = return_highestLanePriority;
        }
      }
    }
  }

  if (nextLanes === NoLanes) {
    // 找了一圈之后发现nextLanes是空的,return一个空
    // This should only be reachable if we're suspended
    // TODO: Consider warning in this path if a fallback timer is not scheduled.
    return NoLanes;
  }

  // If there are higher priority lanes, we'll include them even if they
  // are suspended.
  // 如果有更高优先级的lanes,即使它们被挂起,也会放到nextLanes里。
  nextLanes = pendingLanes & getEqualOrHigherPriorityLanes(nextLanes);
  // nextLanes 实际上是待处理的lanes中优先级较高的那些lanes

  // If we're already in the middle of a render, switching lanes will interrupt
  // it and we'll lose our progress. We should only do this if the new lanes are
  // higher priority.
  /*
* 翻译:如果已经在渲染过程中,切换lanes会中断渲染,将会丢失进程。
* 只有在新lanes有更高优先级的情况下,才应该这样做。(只有在高优先级的任务插队时,才会这样做)
*
* 理解:如果正在渲染,但是新任务的优先级不足,那么不管它,继续往下渲染,只有在新的优先级比当前的正在
* 渲染的优先级高的时候,才去打断(高优先级任务插队)
* */

  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    // If we already suspended with a delay, then interrupting is fine. Don't
    // bother waiting until the root is complete.
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    getHighestPriorityLanes(wipLanes);
    const wipLanePriority = return_highestLanePriority;
    if (nextLanePriority <= wipLanePriority) {
      return wipLanes;
    } else {
      return_highestLanePriority = nextLanePriority;
    }
  }

  // 以下内容暂时未完全理解,翻译仅供参考
  // Check for entangled lanes and add them to the batch.
  // 检查entangled lanes并把它们加入到批处理中
  //
  // A lane is said to be entangled with another when it's not allowed to render
  // in a batch that does not also include the other lane. Typically we do this
  // when multiple updates have the same source, and we only want to respond to
  // the most recent event from that source.
  /*
* 当一个lane禁止在不包括其他lane的批处理中渲染时,它被称为与另一个lane纠缠在一起。通常,当多个更
* 新具有相同的源时,我们会这样做,并且我们只想响应来自该源的最新事件。
* */

  //
  // Note that we apply entanglements *after* checking for partial work above.
  // This means that if a lane is entangled during an interleaved event while
  // it's already rendering, we won't interrupt it. This is intentional, since
  // entanglement is usually "best effort": we'll try our best to render the
  // lanes in the same batch, but it's not worth throwing out partially
  // completed work in order to do it.
  //
  /*
* 注意,我们在检查了上面的部分工作之后应用了纠缠。
这意味着如果一个lane在交替事件中被纠缠,而它已经被渲染,我们不会中断它。这是有意为之,因为纠缠通常
是“最好的努力”:我们将尽最大努力在同一批中渲染lanes,但不值得为了这样做而放弃部分完成的工作。
* */

  // For those exceptions where entanglement is semantically important, like
  // useMutableSource, we should ensure that there is no partial work at the
  // time we apply the entanglement.
  /*
* 对于那些纠缠在语义上很重要的例外,比如useMutableSource,我们应该确保在应用纠缠时没有部分工作。
* */
  const entangledLanes = root.entangledLanes;
  if (entangledLanes !== NoLanes) {
    const entanglements = root.entanglements;
    let lanes = nextLanes & entangledLanes;
    while (lanes > 0) {
      const index = pickArbitraryLaneIndex(lanes);
      const lane = 1 << index;

      nextLanes |= entanglements[index];

      lanes &= ~lane;
    }
  }

  return nextLanes;
}
  • getHighestPriorityLanes

/**
 * 核心功能:从给定 lanes 中找出最高优先级的「预设车道」(上文中列出来的那些) 
*/
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
  // 相关commit https://github.com/facebook/react/pull/19302 ,推荐阅读
  
  // 按照优先级以此对比,找到了就return
  
  if ((SyncLane & lanes) !== NoLanes) {
    // 如果lanes中有同步优先级的任务
    return_highestLanePriority = SyncLanePriority; // return_highestLanePriority 是什么?看下面33 行的注释
    return SyncLane;
  }
  if ((SyncBatchedLane & lanes) !== NoLanes) {
    // 如果lanes中有批量同步的lane
    return_highestLanePriority = SyncBatchedLanePriority;
    return SyncBatchedLane;
  }
  if ((InputDiscreteHydrationLane & lanes) !== NoLanes) {
    // 选出lanes中与InputDiscreteLanes重合的非0位
    return_highestLanePriority = InputDiscreteHydrationLanePriority;
    return InputDiscreteHydrationLane;
  }
  ... // 以下累计类似,都是 if 判断,先省略了
}


// 用于在函数执行过程中存储和传递额外的信息,而不仅仅是通过函数的返回值来传递单个结果。
// `getHighestPriorityLanes` 函数在计算出最高优先级车道的同时,
// 还会将对应的优先级存储在这个特定的变量中,
// 供getNextLanes函数后续使用,这样就实现了 “返回” 多个值的效果。
// 供 getHighestPriorityLanes 和 getNextLanes 函数使用:
let return_highestLanePriority: LanePriority = DefaultLanePriority;

Others

位掩码 bitmask 在 React 中的其他运用

react 中记录执行栈也是用的位掩码

type ExecutionContext = number;

export const NoContext = /*             */ 0b0000000;
const BatchedContext = /*               */ 0b0000001;
const EventContext = /*                 */ 0b0000010;
const DiscreteEventContext = /*         */ 0b0000100;
const LegacyUnbatchedContext = /*       */ 0b0001000;
const RenderContext = /*                */ 0b0010000;
const CommitContext = /*                */ 0b0100000;
export const RetryAfterError = /*       */ 0b1000000;

参考资料