React18源码: React调度中的3种优先级类型和Lane的位运算

优先级类型

  • React内部对于优先级的管理,贯穿运作流程的4个阶段(从输入到输出),根据其功能的不同,可以分为3种类型:
    • 1 )fiber优先级(LanePriority)
      • 位于 react-reconciler包,也就是Lane(车道模型)
    • 2 )调度优先级(SchedulerPriority)
      • 位于scheduler包
    • 3 )优先级等级(ReactPriorityLevel)
      • 位于react-reconciler包中的 SchedulerWithReactIntegration.js
      • 负责上述2套优先级体系的转换.
  • Lane 是在 [email protected]的新特性.

Lane(车道模型)

  • 英文单词lane翻译成中文表示"车道,航道"的意思,所以很多文章都将Lanes模型称为车道模型

  • Lane模型的源码在 ReactFiberLane.js,源码中大量使用了位运算

  • 首先引入对Lane的解释, 这里简单概括如下:

    • 1 )Lane类型被定义为二进制变量,利用了位掩码的特性,在频繁运算的时候占用内存少,计算速度快.

      • Lane和Lanes就是单数和复数的关系,代表单个任务的定义为Lane,代表多个任务的定义为Lanes
    • 2 )Lane是对于expirationTime的重构,以前使用expirationTime表示的字段,都改为了lane

      复制代码
      renderExpirationTime -> renderlanes
      update.expirationTime -> update.lane
      fiber.expirationTime -> fiber.lanes
      fiber.childExpirationTime -> fiber.childLanes
      root.firstPendingTime and root.lastPendingTime -> fiber.pendingLanes
    • 3 )使用Lanes模型相比expirationTime模型的优势

      • Lanes把任务优先级从批量任务中分离出来
      • 可以更方便的判断单个任务与批量任务的优先级是否重叠
      js 复制代码
      // 判断:单task与batchTask的优先级是否重叠
      // 1.通过expirationTime判断
      const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;
      // 2.通过Lanes判断
      const isTaskIncludedInBatch =(task & batchOfTasks) !== 0;
      
      // 当同时处理一组任务,该组内有多个任务,且每个任务的优先级不一致
      // 1. 如果通过expirationTime判断,需要维护一个范围(在Lane重构之前,源码中就是这样比较的)
      const isTaskIncludedInBatch =
        taskPriority <= highestPriorityInRange &&
        taskPriority >= lowestPriorityInRange;
      // 2. 通过Lanes判断
      const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
  • Lanes使用单个32位二进制变量即可代表多个不同的任务

  • 也就是说一个变量即可代表一个组(group)

  • 如果要在一个group中分离出单个task,非常容易

  • 在expirationTime模型设计之初,react体系中还没有 Suspense 异步渲染的概念

  • 现在有如下场景:有3个任务,其优先级A>B>C,正常来讲只需要按照优先级顺序执行就可以了

  • 但是现在情况变了:A和C任务是CPU密集型,而B是IO密集型(Suspense会调用远程api,算是IO任务)

  • 即A(cpu)>B(IO)>C(cpu).此时的需求需要将任务B从group中分离出来,先处理cpu任务A和C

    js 复制代码
    //从group中删除或增加task
    
    // 通过expirationTime实现
    // 维护一个链表,按照单个task的优先级顺序进行插入
    // 删除单个task(从链表中删除一个元素)
    task. prev.next = task.next;
    //2)增加单个task(需要对比当前task的优先级,插入到链表正确的位置上)
    let current = queue;
    while (task.expirationTime >= current.expirationTime) {
      current = current.next;
    }
    task.next = current.next;
    current.next = task;
    //3)比较task是否在group中
    const isTaskIncludedInBatch =
      taskPriority <= highestPriorityInRange &&
      taskPriority >= lowestPriorityInRang;
    //2.通过Lanes实现
    //1)删除单个task
    batchOfTasks &= ~task;
    //2)增加单个task
    batchOfTasks |= task;
    //3)比较task是否在group中
    const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
  • Lanes是一个不透明的类型,只能在ReactFiberLane.js这个模块中维护

  • 如果要在其他文件中使用,只能通过 ReactFiberLane.js 中提供的工具函数来使用

  • 分析车道模型的源码(ReactFiberLane.js中),可以得到如下结论:

    • 1.可以使用的比特位一共有31位
    • 2.共定义了18种车道(Lane/Lanes)变量,每一个变量占有1个或多个比特位,分别定义为Lane和Lanes类型.
    • 3.每一种车道(Lane/Lanes)都有对应的优先级,所以源码中定义了18种优先级(LanePriority).
    • 4.占有低位比特位的Lane变量对应的优先级越高
      • 最高优先级为 SynclanePriority 对应的车道为
        • Synclane = 0b0000000000000000000000000000001
      • 最低优先级为 OffscreenLanePriority 对应的车道为
        • OffscreenLane = 0b1000000000000000000000000000000

位运算

  • 什么是位运算?程序中的所有数在计算机内存中都是以二进制的形式储存的。

  • 位运算就是直接对整数在内存中的二进制位进行操作。

  • 比如

    • 0 在二进制中用 0 表示,我们用 0000 代表;
    • 1 在二进制中用 1 表示,我们用 0001 代表;
  • 那么先看两个位运算符号 & 和 |

    • & 对于每一个比特位,两个操作数都为 1 时,结果为 1,否则为 0
    • | 对于每一个比特位,两个操作数都为0时,结果为 0,否则为 1
    • 我们看一下两个 1 & 0 和 1 | 0
    • 如上 1 & 0 = 0, 1 | 0 = 1
  • 参考

    复制代码
      0       0000
      1       0001
      -------------
      0 & 1 = 0000 = 0
  • 再参考

    复制代码
    0       0000
    1       0001
    ----------------
    0 | 1 = 0001 = 1

使用一张表格详细说明

运算符 用法 描述
与 & a & b 如果两位都是1则设置每位为1
或 | a | b 如果两位之一为1则设置每位为1
异或 ^ a^b 如果两位只有一位为1则设置每位为1
非 ~ ~ a 反转操作数的比特位, 即0变成1, 1变成0
左移(<<) a << b 将a的二进制形式向左移b(< 32)比特位, 右边用0填充
有符号右移(>>) a>>b 将a的二进制形式向右移b(<32)比特位,丢弃被移除的位,左侧以最高位来填充
无符号右移(>>>) a>>>b 将a的二进制形式向右移b(<32)比特位,丢弃被移除的位,并用0在左侧填充

位运算的一个使用场景

  • 比如有一个场景下,会有很多状态常量A,B,C...,这些状态在整个应用中在一些关键节点中做流程控制

  • 比如:

    js 复制代码
    if(value === A) {
      // TODO..
    }
  • 如上判断value等于常量A,那么进入到if的条件语句中

  • 此时是value属性是简单的一对一关系,但是实际场景下value可能是好几个枚举常量的集合

  • 也就是一对多的关系,那么此时value可能同时代表A和B两个属性

  • 如下图所示:

  • 这时候,如果按照下面的代码来写,就会很麻烦

    js 复制代码
    if(value === A || value === B) {
      // TODO..
    }
  • 此时的问题就是如何用一个value表示A和B两个属性的集合,这个时候位运算就派上用场了

  • 因为可以把一些状态常量用32位的二进制来表示(这里也可以用其他进制),比如:

    js 复制代码
    const A = 0b0000000000000000000000000000001 // 优先级最高
    const B = 0b0000000000000000000000000000010
    const C = 0b0000000000000000000000000000100 // 优先级最低
  • 通过移位的方式让每一个常量都单独占一位,这样在判断一个属性是否包含常量的时候

  • 可以根据当前位数的1和0来判断

  • 这样如果一个值即代表A又代表B那么就可以通过位运算的 | 来处理

  • 就有 AB = A | B = 0b0000000000000000000000000000011

  • 那么如果把AB的值赋予给value,那么此时的value就可以用来代表A和B

  • 此时当然不能直接通过等于或者恒等来判断value是否为A或者B,此时就可以通过&来判断。具体实现如下:

    js 复制代码
    const A = 0b0000000000000000000000000000001
    const B = 0b0000000000000000000000000000010
    const C = 0b0000000000000000000000000000100
    const N = 0b0000000000000000000000000000000
    const value = A | B
    console.log((value & A ) !== N) // true
    console.log((value & B ) !== N) // true
    console.log((value & C ) !== N ) // false

位运算在 react 中的应用

js 复制代码
export const NoLanes = /*                */ 0b0000000000000000000000000000000;
const SyncLane = /*                      */ 0b0000000000000000000000000000001;

const InputContinuousHydrationLane = /*  */ 0b0000000000000000000000000000010;
const InputContinuousLane = /*           */ 0b0000000000000000000000000000100;

const DefaultHydrationLane = /*          */ 0b0000000000000000000000000001000;
const DefaultLane = /*                   */ 0b0000000000000000000000000010000;

const TransitionHydrationLane = /*       */ 0b0000000000000000000000000100000;
const TransitionLane = /*                */ 0b0000000000000000000000001000000;
  • 如上 SyncLane 代表的数值是1,它却是最高的优先级

  • 也即是说lane的代表的数值越小,此次更新的优先级就越大

  • 在新版本的React中,还有一个新特性,就是render阶段可能被中断,在这个期间会产生一个更高优先级的任务

  • 那么会再次更新lane属性,这样多个更新就会合并,这样一个lane可能需要表现出多个更新优先级

  • 我们来看一下React是如何通过位运算分离出优先级的

    js 复制代码
    function getHighestPriorityLane(lanes) {
      return lanes & -lanes;
    }
  • 如上就是通过 lanes & -lanes 分离出最高优先级的任务的,我们来看一下具体的流程

    • 比如SyncLane和InputContinuousLane合并之后的任务优先级lane为
    • SyncLane = 0b0000000000000000000000000000001
    • InputContinuousLane = 0b0000000000000000000000000000100
    • lane = SyncLane|InputContinuousLane
    • lane = 0b0000000000000000000000000000101
    • 那么通过 lanes & -lanes 分离出 SyncLane
    • 首先我们看一下 -lanes,在二进制中需要用补码表示为:
    • -lane = 0b1111111111111111111111111111011
    • 那么接下来执行 lanes & -lanes 看一下,& 的逻辑是如果两位都是1则设置改位为1,否则为0
    • 那么 lane & -lane, 只有一位(最后一位)全是1,所有合并后的内容为:
    • lane & -lane = 0b0000000000000000000000000000001
  • 可以看得出来lane&-lane的结果是SyncLane,所以通过lane&-lane就能分离出最高优先级的任务

    js 复制代码
    const SyncLane = 0b0000000000000000000000000000001
    const InputContinuousLane = 0b0000000000000000000000000000100
    const lane = SyncLane | InputContinuousLane
    console.log((lane & -lane) === SyncLane ) // true
相关推荐
Apifox7 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿35 分钟前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼1 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下1 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox1 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758102 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox
掘金一周2 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队2 小时前
Vue自定义指令最佳实践教程
前端·vue.js