likes
comments
collection
share

React 源码专栏之 React 任务优先级

作者站长头像
站长
· 阅读数 44

React 优先级管理是 React 的 Fiber 架构中一个关键部分,它使得可中断渲染、时间切片和异步渲染(Suspense)成为可能。通过优先级管理,React 能够确保高优先级的任务(如用户输入)得到及时响应,从而提供更流畅的用户体验。

不同优先级的任务间,会存在一种现象,当执行低优先级任务时,突然插入一个高优先级任务,那么会中断低优先级的任务,先执行高优先级的任务,我们可以将这种现象称为任务插队。

当高优先级任务执行完,准备执行低优先级任务时,又插入一个高优先级任务,那么又会执行高优先级任务,如果不断有高优先级任务插队执行,那么低优先级任务便一直得不到执行,我们称这种现象为任务饥饿问题。(对于饥饿问题,我们会在后面的内容中讲到如何处理)

当用户操作界面时,例如搜索、点击、下拉等操作,为了避免页面卡顿,需要让出线程的执行权,优先执行用户触发的事件,这个我们称之为高优先级任务,其它不那么重要的事件我们称之为低优先级任务。

优先级分类

在整个 React 内部中对于优先级的管理,根据其功能的不同分为了三种优先级:

  1. lane 优先级:

    • SyncLane:同步优先级,立即执行的更新,例如用户输入。

    • InputContinuousLane:输入连续性优先级,用于确保输入的连贯性,如拖动操作。

    • DefaultLane:默认优先级,通常用于正常的状态更新。

    • TransitionLane:过渡优先级,用于异步更新,如 Suspense 和并发模式中的更新。

    • IdleLane:空闲优先级,仅在主线程完全空闲时执行。

  2. React 事件优先级:

    • Discrete Events:离散事件,例如点击、按键等,这些事件需要立即响应。

    • Continuous Events:连续事件,例如滚动、鼠标移动等,这些事件需要持续响应。

    • Default Events:默认事件,例如一些不太紧急的状态更新。

  3. Schedule 优先级:

    • Immediate Priority:立即优先级,最高优先级的任务,必须立即执行。

    • User Blocking Priority:用户阻塞优先级,需要尽快完成以确保用户体验流畅。

    • Normal Priority:正常优先级,普通的任务,稍微延迟也没关系。

    • Low Priority:低优先级,可以显著延迟的任务。

    • Idle Priority:空闲优先级,仅在主线程完全空闲时执行的任务。

上面只是列举了一些常见的,还有很多优先级的,具体的会在下面的内容中讲到。

React 中的优先级管理通过 schedule 调度器和 Fiber 架构实现。schedule 调度器负责管理任务的执行顺序,Fiber 架构则用于表示和管理组件树的更新。

lane 优先级

在 React 中,lane 是用来标识更新优先级的位掩码,它可以在频繁运算的时候占用内存少,计算速度快。每个 lane 代表一种优先级级别,React 可以同时处理多个 lane,通过这种方式来管理不同优先级的任务。

回想一下我们学校的操场,是不是分为了多个跑道,跑道越往内,距离越近,lane 模型借助了这个概念,将其分为了 31 个赛道,其中位数越少的赛道,也就是 1 越往右的赛道有优先级 就越高,某些相邻的赛道拥有相同的 优先级,称为赛道组。

在 18 版本中,React 有 31 中 lane,如下代码所示:

export const TotalLanes = 31;

//  表示没有车道,所有为都为0
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;

// 事务车道 16条
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;

const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /*                       */ 0b0000000001000000000000000000000;

// 重试车道
const RetryLanes: Lanes = /*                            */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /*                             */ 0b0000100000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;

// 选着性注水车道
export const SelectiveHydrationLane: Lane = /*          */ 0b0001000000000000000000000000000;

// 非空闲车道
const NonIdleLanes: Lanes = /*                          */ 0b0001111111111111111111111111111;

// 空闲注水车道和空闲车道
export const IdleHydrationLane: Lane = /*               */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0100000000000000000000000000000;

// 离屏渲染车道
export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

如果把所有的 lane 从低位到高位排列到一块,我们就能看到这些数字会从低位到高位依次出现在对应的位置上。

我们来特别关注一下上面优先级中的过渡优先级:

const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;

过渡优先级分配规则是:分配优先级时,会从过渡优先级的最右边开始分配,后续产生的任务则会依次向左移动一位,直到最后一个位置被分配后,后面的任务会从最右边第一个位置再开始做分配

当前产生了一个任务 1,那么会分配过渡优先级的最右边第一个位置:

TransitionLane1 = 0b0000000000000000000000001000000;

现在又产生了任务 2,那么则从 A 的位置向左移动一位:

TransitionLane2 = 0b0000000000000000000000010000000;

后续产生的任务则会依次向左移动一位,过渡优先级共有 16 位:

TransitionLanes = 0b0000000001111111111111111000000;

在复杂的 React 应用中,可能同时存在多个过渡任务,这些任务可能属于不同的优先级。通过合并多个 TransitionLane,React 调度器可以灵活地处理这些任务,而不需要分别调度每个优先级的任务。

合并优先级 lane 可以减少调度器的开销。调度器可以一次性检查多个优先级的任务,避免了多次遍历任务队列,从而提高性能。

假设我们有以下几个过渡优先级 lane:

const TransitionLane1 = 0b0000000000000000000000000010000; // 16
const TransitionLane2 = 0b0000000000000000000000000100000; // 32
const TransitionLane3 = 0b0000000000000000000000001000000; // 64
const TransitionLane4 = 0b0000000000000000000000010000000; // 128

每个位表示一个独立的优先级 lane。通过按位或(OR)操作,可以将多个优先级 lane 合并在一起:

const combinedTransitionLanes = TransitionLane1 | TransitionLane2;

combinedTransitionLanes 的值是 0b0000000000000000000000000110000,表示同时包含 TransitionLane1 和 TransitionLane2。

除此之外,我们可以通过按位与(AND)操作来判断某个任务是否属于某个优先级 lane,并通过按位与非(AND NOT��操作来删除某个优先级 lane。

const TransitionLane1 = 0b0000000000000000000000000010000; // 16
const TransitionLane2 = 0b0000000000000000000000000100000; // 32
const TransitionLane3 = 0b0000000000000000000000001000000; // 64
const TransitionLane4 = 0b0000000000000000000000010000000; // 128

const TransitionLanes = 0b0000000001111111111111111000000;

// 判断某个任务是否属于任务组
const isInTransitionLanes1 = (TransitionLanes & TransitionLane1) !== 0;
console.log(`TransitionLane1 属于任务组: ${isInTransitionLanes1}`); // 输出 true 或 false

const isInTransitionLanes2 = (TransitionLanes & TransitionLane2) !== 0;
console.log(`TransitionLane2 属于任务组: ${isInTransitionLanes2}`); // 输出 true 或 false

const isInTransitionLanes3 = (TransitionLanes & TransitionLane3) !== 0;
console.log(`TransitionLane3 属于任务组: ${isInTransitionLanes3}`); // 输出 true 或 false

const isInTransitionLanes4 = (TransitionLanes & TransitionLane4) !== 0;
console.log(`TransitionLane4 属于任务组: ${isInTransitionLanes4}`); // 输出 true 或 false

// 删除某个过渡任务
const removedTransitionLane1 = TransitionLanes & ~TransitionLane1;
console.log(
  `删除 TransitionLane1 后的任务组: ${removedTransitionLane1.toString(2)}`
);

const removedTransitionLane2 = TransitionLanes & ~TransitionLane2;
console.log(
  `删除 TransitionLane2 后的任务组: ${removedTransitionLane2.toString(2)}`
);

lane 的基本运算就是一些基础的集合运行,主要有以下这些函数:

// src/react/packages/react-reconciler/src/ReactFiberLane.old.js

// 获取指定 Lanes 中的任意一个 Lane 的索引
function pickArbitraryLaneIndex(lanes: Lanes) {
  return 31 - clz32(lanes);
}

// 将单个 Lane 转换为其索引
function laneToIndex(lane: Lane) {
  return pickArbitraryLaneIndex(lane);
}

// 判断两个 lanes 集合之间是否有交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
  return (a & b) !== NoLanes;
}

// 判断两个 lanes 集合是否是父子集的关系
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset;
}

// 合并两个 lanes 集合
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}

// 从一个 lanes 集合中把某个 lane 或者 lanes 集合给移除掉,相当于求一个集合的补集
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
  return set & ~subset;
}

// 求两个 lanes 集合的交集
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a & b;
}

React 事件优先级

React 事件优先级(Event Priorities)是 React 调度机制的一部分,用于管理和协调用户事件的处理顺序。通过为不同类型的事件分配不同的优先级,React 可以确保关键事件得到及时处理,而低优先级的事件可以在不影响用户体验的情况下延迟处理。 React 事件优先级主要分为以下几类:

  1. Discrete Events(离散事件):立即响应的用户交互事件,例如点击、按键。这些事件需要快速响应,不能被延迟处理。

  2. Continuous Events(连续事件):需要持续响应的事件,例如滚动、拖动。这些事件可以被分段处理,确保流畅的用户体验。

  3. Default Events(默认事件):常规优先级的事件,不需要立即响应。例如数据加载、页面渲染。

  4. Idle Events(空闲事件):仅在浏览器空闲时执行的事件。例如后台数据同步。

// src/react/packages/react-reconciler/src/ReactEventPriorities.old.js

// 离散事件优先级,例如:点击事件,input输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane;

Schedule 优先级

React 的调度机制使用了一种称为“优先级 lane”的机制来管理任务的优先级。Schedule 调度器会根据不同的优先级来决定任务的执行顺序和时间。

在 React Scheduler 中,有六种主要的优先级类型,每种类型对应一个整数值:

  1. NoPriority (0):无优先级,表示不需要立即执行的任务。

  2. ImmediatePriority (1):立即优先级,表示必须立即执行的任务,不能被打断。

  3. UserBlockingPriority (2):用户阻塞优先级,表示需要快速响应但可以被更高优先级任务打断的任务。

  4. NormalPriority (3):正常优先级,表示一般的任务,可以被更高优先级的任务打断。

  5. LowPriority (4):低优先级,表示不紧急的任务,可以在空闲时间处理。

  6. IdlePriority (5):空闲优先级,仅在浏览器空闲时执行的任务。

这些优先级用于 React 调度器决定哪些任务应该首先执行,哪些任务可以延迟执行。

// 无优先级
export const NoPriority = 0;
// 立即执行优先级
export const ImmediatePriority = 1;
// 用户阻塞操作优先级 点击,输入
export const UserBlockingPriority = 2;
// 正常优先级
export const NormalPriority = 3;
// 低优先级
export const LowPriority = 4;
// 空闲优先级
export const IdlePriority = 5;

优先级转换关系

在整个 React 应用当中,它们 key 分为四种优先级,它们分别有如下优先级:

  1. 事件优先级: 按照用户事件的交互紧急程度,划分的优先级;

  2. 更新优先级:事件导致 React 产生的更新对象 update 的优先级;

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

  4. 调度优先级: Schedule 依据 React 更新任务生成一个调度任务,这个调度任务所持有的优先级;

前三者属于 React 的优先级机制,第四个属于 Scheduler 的优先级机制,Scheduler 内部有自己的优先级机制,虽然与 React 有所区别,但等级的划分基本一致。

Lane 优先级转换为 React 事件优先级如下代码所示:

// src/react/packages/react-reconciler/src/ReactEventPriorities.old.js

// lanes模型优先级转换为事件优先级
export function lanesToEventPriority(lanes: Lanes): EventPriority {
  const lane = getHighestPriorityLane(lanes);
  if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
  }
  if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
  }
  if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
  }
  return IdleEventPriority;
}

React 事件优先级转换为 Scheduler 优先级如下代码所示:

React 源码专栏之 React 任务优先级

它主要是在 src/react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js 文件中的 ensureRootIsScheduled 函数。

lanesToEventPriority 函数就是上面 Lane 优先级转换为 React 事件优先级的函数,先将 lane 的优先级转换为 React 事件的优先级,然后再根据 React 事件的优先级转换为 Scheduler 的优先级。

总结

通过本文的学习我们应该对 React 的任务优先级有一个简单的了解了,以及 lane 模型采用位运算的方式取代了之前的 expirationTime 模型,减少了内存的占用,而从运算的性能来看,按位运算肯定是高效于普通的算术运算。

正是通过优先级的灵活运用, React 实现了可中断渲染,时间切片(time slicing),异步渲染(suspense)等特性.

更多的优势将会从后面的内容中更好地体现出来。虽然本篇文章内容结束了,但是任务优先级这里还远远没有技术,这里还设计了 schedule 调度引起的饥饿问题,还有 lane 模型的生命周期,这些都将会从后面的内容或者单独一篇的内容提炼出来。

转载自:https://juejin.cn/post/7394284803749101568
评论
请登录