React Lane模型:优先级与批处理的解耦革命

lane模型

面试题:是否了解过 React 中的 lane 模型?为什么要从之前的 expirationTime 模型转换为 lane 模型?

React 和 Scheduler 优先级的介绍

之前我们已经介绍过 Scheduler,React 团队是打算将 Scheduler 进行独立发布。

在 React 内部,还会有一个粒度更细的优先级算法,这个就是 lane 模型。

接下来我们来看一下两套优先级模型的一个转换。

在 Scheduler 内部,拥有 5 种优先级:

javascript 复制代码
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

作为一个独立的包,需要考虑到通用性,Scheduler 和 React 的优先级并不共通,在 React 内部,有四种优先级,如下四种:

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

由于 React 中不同的交互对应的事件回调中产生的 update 会有不同的优先级,因此优先级与事件有关,因此在 React 内部的优先级也被称之为 EventPriority,各种优先级的含义如下:

  • DiscreteEventPriority:对应离散事件优先级,例如 click、input、focus、blur、touchstart 等事件都是离散触发的
  • ContinuousEventPriority:对应连续事件的优先级,例如 drag、mousemove、scroll、touchmove 等事件都是连续触发的
  • DefaultEventPriority:对应默认的优先级,例如通过计时器周期性触发更新,这种情况下产生的 update 不属于交互产生 update,所以优先级是默认的优先级
  • IdleEventPriority:对应空闲情况的优先级

在上面的代码中,我们还可以观察出一件事情,不同级别的 EventPriority 对应的是不同的 lane

既然 React 与 Scheduler 优先级不互通,那么这里就会涉及到一个转换的问题,这里分为:

  • React 优先级转为 Scheduler 的优先级
  • Scheduler 的优先级转为 React 的优先级

React 优先级转为 Scheduler 的优先级

整体会经历两次转换:

  • 首先是将 lanes 转为 EventPriority,涉及到的方法如下:
javascript 复制代码
export function lanesToEventPriority(lanes: Lanes): EventPriority {
  // getHighestPriorityLane 方法用于分离出优先级最高的 lane
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}
  • 将 EventPriority 转换为 Scheduler 的优先级,方法如下:
javascript 复制代码
// ...
let schedulerPriorityLevel;
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;
}
// ...

举一个例子,假设现在有一个点击事件,在 onClick 中对应有一个回调函数来触发更新,该更新属于 DiscreteEventPriority,经过上面的两套转换规则进行转换之后,最终得到的 Scheduler 对应的优先级就是 ImmediateSchedulerPriority

Scheduler 的优先级转为 React 的优先级

转换相关的代码如下:

javascript 复制代码
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
  case ImmediateSchedulerPriority:
    return DiscreteEventPriority;
  case UserBlockingSchedulerPriority:
    return ContinuousEventPriority;
  case NormalSchedulerPriority:
  case LowSchedulerPriority:
    return DefaultEventPriority;
  case IdleSchedulerPriority:
    return IdleEventPriority;
  default:
    return DefaultEventPriority;
}

这里会涉及到一个问题,在同一时间可能存在很多的更新,究竟先去更新哪一个?

  • 从众多的有优先级的 update 中选出一个优先级最高的
  • 表达批的概念

React 在表达方式上面实际上经历了两次迭代:

  • 基于 expirationTime 的算法
  • 基于 lane 的算法

expirationTime 模型

React 早期采用的就是 expirationTime 的算法,这一点和 Scheduler 里面的设计是一致的。

在 Scheduler 中,设计了 5 种优先级,不同的优先级会对应不同的 timeout,最终会对应不同的 expirationTime,然后 task 根据 expirationTime 来进行任务的排序。

早期的时候在 React 中延续了这种设计,update 的优先级与触发事件的当前时间以及优先级对应的延迟时间相关,这样的算法实际上是比较简单易懂的,每当进入 schedule 的时候,就会选出优先级最高的 update 进行一个调度。

但是这种算法在表示"批"的概念上不够灵活。

在基于 expirationTime 模型的算法中,有如下的表达:

ini 复制代码
const isUpdateIncludedInBatch = priorityOfUpdate >= priorityOfBatch;

priorityOfUpdate 表示的是当前 update 的优先级,priorityOfBatch 代表的是批对应的优先级下限,也就是说,当前的 update 只要大于等于 priorityOfBatch,就会被划分为同一批:

但是此时就会存在一个问题,如何将某一范围的某几个优先级划为同一批?

在上图中,我们想要将 u1、u2、u3 和 u4 划分为同一批,但是以前的 expirationTime 模型是无法做到的。

究其原因,是因为 expirationTime 模型优先级算法耦合了"优先级"和"批"的概念,限制了模型的表达能力。优先级算法的本质是为 update 进行一个排序,但是 expirationTime 模型在完成排序的同时还默认的划定了"批"。

lane 模型

因此,基于上述的原因,React 中引入了 lane 模型。

不管新引入什么模型,比如要保证以下两个问题得到解决:

  • 以优先级为依据,对 update 进行一个排序
  • 表达批的概念

针对第一个问题,lane模型中设置了很多的 lane,每一个lane实际上是一个二进制数,通过二进制来表达优先级,越低的位代表越高的优先级,例如:

javascript 复制代码
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000010000;
export const IdleLane: Lane = /*                        */ 0b0100000000000000000000000000000;
export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

在上面的代码中,SyncLane 是最高优先级,OffscreenLane 是最低优先级。

对于第二个问题,lane模型能够非常灵活的表达批的概念:

ini 复制代码
// 要使用的批
let batch = 0;
// laneA 和 laneB。是不相邻的优先级
const laneA = 0b0000000000000000000000001000000;
const laneB = 0b0000000000000000000000000000001;
// 将 laneA 纳入批中
batch |= laneA;
// 将 laneB 纳入批中
batch |= laneB;

真题解答

题目:是否了解过 React 中的 lane 模型?为什么要从之前的 expirationTime 模型转换为 lane 模型?

参考答案:

在 React 中有一套独立的粒度更细的优先级算法,这就是 lane 模型。

这是一个基于位运算的算法,每一个 lane 是一个 32 bit Integer,不同的优先级对应了不同的 lane,越低的位代表越高的优先级。

早期的 React 并没有使用 lane 模型,而是采用的的基于 expirationTime 模型的算法,但是这种算法耦合了"优先级"和"批"这两个概念,限制了模型的表达能力。优先级算法的本质是"为 update 排序",但 expirationTime 模型在完成排序的同时还默认的划定了"批"。

使用 lane 模型就不存在这个问题,因为是基于位运算,所以在批的划分上会更加的灵活。

相关推荐
阳光是sunny1 分钟前
走进AI(1):细说RAG、MCP、Agent、Function Call
前端·ai编程
剪刀石头布啊10 分钟前
var、let、const与闭包、垃圾回收
前端·javascript
剪刀石头布啊11 分钟前
js常见的单例
前端·javascript
剪刀石头布啊12 分钟前
数据口径
前端·后端·程序员
剪刀石头布啊16 分钟前
http状态码大全
前端·后端·程序员
剪刀石头布啊18 分钟前
iframe通信、跨标签通信的常见方案
前端·javascript·html
宇之广曜27 分钟前
搭建 Mock 服务,实现前端自调
前端·mock
yuko093129 分钟前
【手机验证码】+86垂直居中的有趣问题
前端
用户15129054522033 分钟前
Springboot中前端向后端传递数据的几种方式
前端
阿星做前端33 分钟前
如何构建一个自己的 Node.js 模块解析器:node:module 钩子详解
前端·javascript·node.js