☰
图解 React 源码之位运算:位掩码、Lanes 车道模型与 ExecutionContext 执行上下文
2026/9/27 7:41:19 网站建设 项目流程
  • 教程
  • 前端

【免费下载链接】react-illustration-series

图解react源码, 用大量配图的方式, 致力于将react原理表述清楚.

项目地址:https://gitcode.com/gh_mirrors/re/react-illustration-series
点击查看免费下载

位运算直接操作每一个比特位(bit),是 React 源码中最高频的基础算法之一:react@17引入的车道模型Lanes(优先级管理)与react-reconciler包中的ExecutionContext(执行上下文状态机)都完全建立在位掩码(bitmask)之上。本文以 docs/algorithm/bitfield.md 为主线,先温习 JS 位运算的规范与注意事项,再结合react@17.0.2源码中ReactFiberLane.js与ReactFiberWorkLoop.old.js的真实实现,拆解lanes & -lanes、31 - clz32(lanes)、executionContext & (RenderContext | CommitContext)等经典位运算技巧,读完即可无障碍阅读 React 的优先级调度与 fiber 树构造源码。

位运算的概念与特性

位运算直接处理每一个比特位(bit),是非常底层的运算。它的优势是速度快,劣势是不直观且只支持整数运算。React 源码之所以大量使用位运算,正是看重其在频繁运算场景下的性能优势,以及位掩码表达二进制变量关系的简洁性。

JS 中一共有 7 种位运算操作符,完整整理如下:

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

JS 中的位运算:Int32 规则与注意事项

ES5 规范:结果是有符号 32 位整型

在 ES5 规范(ECMA-262 5.1 版Binary Bitwise Operators一节)中,对二进制位运算的求值过程做了明确规定:

The production A : A @ B, where @ is one of the bitwise operators in the productions above, is evaluated as follows: 1. Let lref be the result of evaluating A. 2. Let lval be GetValue(lref). 3. Let rref be the result of evaluating B. 4. Let rval be GetValue(rref). 5. Let lnum be ToInt32(lval). 6. Let rnum be ToInt32(rval). 7. Return the result of applying the bitwise operator @ to lnum and rnum. The result is a signed 32 bit integer.

意思是:位运算的左右操作数都会被转换为有符号 32 位整型(Int32),返回结果同样是有符号 32 位整型。由此可以推导出两点:

  • 当操作数是浮点型时,首先会被转换成整型,再进行位运算;
  • 当操作数过大、超过了 Int32 范围,超过的部分会被截取。

归纳起来,理解 JS 位运算需要把握 3 个要点:

  1. 位运算只能在整型变量之间进行运算;
  2. js 中的Number类型在底层都是以浮点数(参考 IEEE754 标准)进行存储的;
  3. js 中所有的按位操作符的操作数都会被转成补码(two's complement)形式的有符号 32 位整数。

两种造成结果异常的情况

在 js 中使用位运算时,有 2 种情况会造成结果异常:

  1. 操作数为浮点型(虽然底层都是浮点型,此处理解为显式的浮点型)

    • 转换流程:浮点数 → 整数(丢弃小数位)→ 位运算
  2. 操作数的大小超过 Int32 范围(-2^31 ~ 2^31-1)。超过范围的二进制位会被截断,取低位 32bit:

    Before: 11100110111110100000000000000110000000000001 After: 10100000000000000110000000000001

这一点直接决定了 React 源码中Lanes变量为什么最多只能使用 31 位(见下文)。

隐式转换

由于 js 语言的隐式转换机制,对非Number类型使用位运算操作符时会发生隐式转换,相当于先使用Number(xxx)将其转换为number类型,再进行位运算:

'str' >>> 0; // ===> Number('str') >>> 0 ===> NaN >>> 0 = 0

基本使用:枚举与位掩码

为了方便比较,以下演示代码中的注释都写成 8 位二进制数(上文已经说明,事实上在 js 中位运算最终的结果都是 Int32)。

枚举属性:通过位移的方式,定义一些枚举常量。

const A = 1 << 0; // 0b00000001 const B = 1 << 1; // 0b00000010 const C = 1 << 2; // 0b00000100

位掩码(bitmask):通过位移定义的一组枚举常量,可以利用位掩码的特性快速操作这些枚举常量(增加、删除、比较)。

  1. 属性增加|:ABC = A | B | C
  2. 属性删除& ~:AB = ABC & ~C
  3. 属性比较:
    • AB 当中包含 B:AB & B === B
    • AB 当中不包含 C:AB & C === 0
    • A 和 B 相等:A === B
const A = 1 << 0; // 0b00000001 const B = 1 << 1; // 0b00000010 const C = 1 << 2; // 0b00000100 // 增加属性 const ABC = A | B | C; // 0b00000111 // 删除属性 const AB = ABC & ~C; // 0b00000011 // 属性比较 // 1. AB当中包含B console.log((AB & B) === B); // true // 2. AB当中不包含C console.log((AB & C) === 0); // true // 3. A和B相等 console.log(A === B); // false

这套「位移定义常量 + 位掩码增删查」的组合拳,正是 React 源码中Lanes与ExecutionContext的实现基石。

React 源码中的高频应用

在 react 核心包中,位运算的使用场景非常多,以下列举使用频率最高、最具代表性的两类:优先级管理lanes与执行上下文ExecutionContext。更完整的优先级体系与 fiber 树构造中的位运算应用,可继续阅读 优先级管理 与 fiber 树构造(基础准备) 两章。

车道模型 Lanes:优先级管理

lanes是react@17.x版本中开始引入的重要概念,代替了16.x版本中的expirationTime,作为fiber对象的一个属性(位于react-reconciler包),主要控制 fiber 树在构造过程中的优先级。Lane模型把优先级从「基于数值比较的过期时间」重构为「基于位掩码的二进制变量」——一个 32 位二进制变量即可代表多个不同的任务(即一个group),要在一个 group 中分离出单个 task 也非常容易,例如判断单任务与批量任务的优先级是否重叠,从priorityOfTask >= priorityOfBatch的数值比较简化为(task & batchOfTasks) !== 0的一次按位与。

Lane和Lanes是单数与复数的关系:代表单个任务的定义为Lane,代表多个任务的定义为Lanes。二者都是number类型的不透明类型,只能在ReactFiberLane.js模块内维护,其他文件只能通过该模块暴露的工具函数使用(详见 优先级管理 中对车道模型的深入解读)。

变量定义

源码ReactFiberLane.js中的定义如下:

//类型定义 export opaque type Lanes = number; export opaque type Lane = number; // 变量定义 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; // ... // ... const NonIdleLanes = /* */ 0b0000111111111111111111111111111; export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000; export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;

源码中Lanes和Lane都是number类型,并且所有变量都使用二进制位来表示。

注意:源码中变量只列出了 31 位,由于 js 中位运算都会转换成 Int32(上文已经解释),最多为 32 位,且最高位是符号位。所以除去符号位,最多只有 31 位可以参与运算。这正对应了「可使用的比特位一共有 31 位」这一约束(完整推导见 优先级管理)。

在 31 位可用的比特位中,React 共定义了 18 种车道(Lane/Lanes)变量,每一个变量占有 1 个或多个比特位;占有低位比特位的Lane变量对应的优先级越高——最高优先级为SyncLane = 0b...0001,最低优先级为OffscreenLane = 0b1000...0000。

方法定义:位掩码判断

ReactFiberLane.js中基于位掩码特性的方法定义如下:

function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { // 判断 lanes中是否包含 SyncLane if ((SyncLane & lanes) !== NoLanes) { return_highestLanePriority = SyncLanePriority; return SyncLane; } // 判断 lanes中是否包含 SyncBatchedLane if ((SyncBatchedLane & lanes) !== NoLanes) { return_highestLanePriority = SyncBatchedLanePriority; return SyncBatchedLane; } // ... // ... 省略其他代码 return lanes; }

在方法定义中,也是通过位掩码的特性来判断二进制形式变量之间的关系。除了常规的位掩码操作外,特别说明其中 2 个技巧性很强的函数:

getHighestPriorityLane:分离出最高优先级

function getHighestPriorityLane(lanes: Lanes) { return lanes & -lanes; }

通过lanes & -lanes可以分离出所有比特位中最右边的 1(补码取负再按位与),具体来讲:

  • 假设lanes(InputDiscreteLanes) = 0b0000000000000000000000000011000
  • 那么-lanes = 0b1111111111111111111111111101000
  • 所以lanes & -lanes = 0b0000000000000000000000000001000
  • 相比最初的 InputDiscreteLanes,分离出来了最右边的 1
  • 通过 lanes 的定义,数字越小的优先级越高,所以此方法可以获取最高优先级的 lane

getLowestPriorityLane:分离出最低优先级

function getLowestPriorityLane(lanes: Lanes): Lane { // This finds the most significant non-zero bit. const index = 31 - clz32(lanes); return index < 0 ? NoLanes : 1 << index; }

clz32(lanes)返回一个数字在转换成 32 位无符号整形数字的二进制形式后,前导 0 的个数(count leading zeros)。逐步推导:

  • 假设lanes(InputDiscreteLanes) = 0b0000000000000000000000000011000
  • 那么clz32(lanes) = 27:由于 InputDiscreteLanes 在源码中被书写成了 31 位,虽然字面上前导 0 是 26 个,但转成标准 32 位后是 27 个
  • index = 31 - clz32(lanes) = 4
  • 最后1 << index = 0b0000000000000000000000000010000
  • 相比最初的 InputDiscreteLanes,分离出来了最左边的 1
  • 通过 lanes 的定义,数字越小的优先级越高,所以此方法可以获取最低优先级的 lane

这两个函数在fiber树构造中频繁出现,例如findUpdateLane、getNextLanes等都会用到「取最高/最低优先级车道」的能力。

ExecutionContext:执行上下文状态机

ExecutionContext定义于react-reconciler包,代表reconciler在运行时的上下文状态(如是否处于 render、commit、事件处理等阶段)。它也是一个二进制变量,通过位运算来维护其状态(在 栈操作 一章中,还揭示了它与「函数调用栈」的巧妙配合)。

变量定义

ReactFiberWorkLoop.old.js中的定义如下:

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; // ... // Describes where we are in the React execution stack let executionContext: ExecutionContext = NoContext;

注意:和lanes的定义不同,ExecutionContext类型的变量在定义的时候采用的是8 位二进制表示(因为变量的数量少,8 位就够了,没有必要写成 31 位)。这 8 种状态即 fiber 树构造(基础准备) 中所归纳的「执行栈」,在reconciler运作流程的每个阶段都会通过位运算改写它:render 之前设置executionContext |= RenderContext,commit 之前设置executionContext |= CommitContext。

scheduleUpdateOnFiber 中的位运算判断

使用位运算的地方较多,这里举一个代表性最强的例子:scheduleUpdateOnFiber是react-reconciler包对react包暴露出来的 API,每一次更新(无论首次渲染还是后续更新)都会调用它,是输入链路的必经之路(其在整体流程中的位置见 reconciler 运作流程)。

// scheduleUpdateOnFiber函数中包含了好多关于executionContext的判断(都是使用位运算) export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, ) { if (root === workInProgressRoot) { // 判断: executionContext 不包含 RenderContext if ( deferRenderPhaseUpdateToNextBatch || (executionContext & RenderContext) === NoContext ) { // ... } } if (lane === SyncLane) { if ( // 判断: executionContext 包含 LegacyUnbatchedContext (executionContext & LegacyUnbatchedContext) !== NoContext && // 判断: executionContext 不包含 RenderContext或CommitContext (executionContext & (RenderContext | CommitContext)) === NoContext ) { // ... } } // ... }

这里展示了位掩码的两种典型判断姿势:

  • (executionContext & RenderContext) === NoContext:判断不包含某个状态(按位与结果为 0);
  • (executionContext & LegacyUnbatchedContext) !== NoContext:判断包含某个状态(按位与结果非 0);
  • (executionContext & (RenderContext | CommitContext)) === NoContext:先用|把多个状态合并成一个掩码,再一次性判断既不包含 A 也不包含 B——这是「组合掩码 + 按位与」的经典用法。

位运算在优先级体系中的延伸

理解lanes与executionContext两个基础之后,位运算在 React 源码中还有三处高频延伸应用(均可在 fiber 树构造(基础准备) 中找到源码展开):

  1. update优先级(update.lane):无论是应用初始化(updateContainer)还是setState发起更新,都会调用requestUpdateLane(fiber)生成一个lane。其中 legacy 模式直接返回SyncLane;concurrent 模式在(executionContext & DiscreteEventContext) !== NoContext且调度优先级为UserBlockingSchedulerPriority时,走findUpdateLane(InputDiscreteLanePriority, ...)输入事件通道——又是一个「位掩码判断执行上下文」的实例。

  2. 渲染优先级(renderLanes):每次 render 之前调用getNextLanes(root, wipLanes),根据fiberRoot上的pendingLanes、expiredLanes、suspendedLanes、pingedLanes等属性确定本次渲染的优先级,内部大量使用pendingLanes & NonIdleLanes这类掩码过滤逻辑。

  3. fiber节点是否复用的判断(beginWork中的 bailout):在beginWork中,如果oldProps === newProps且!includesSomeLane(renderLanes, updateLanes),则说明本 fiber 节点没有更新、可以复用,直接进入bailoutOnAlreadyFinishedWork跳过整棵子树——这里的includesSomeLane本质就是(lane & lanes) !== NoLanes。

此外,借助executionContext === NoContext这一位运算判断,fiber 树构造(基础准备) 中还给出了「setState到底是同步还是异步」的源码级标准答案:如果逻辑进入flushSyncCallbackQueue(此时executionContext === NoContext),会主动取消调度并立即进入 fiber 树构造,setState表现为同步;正常情况下不取消schedule调度,由于调度通过MessageChannel(宏任务)触发,setState表现为异步。

总结

本节介绍了位运算的基本使用,并列举了位运算在 react 源码中的高频应用。在特定的情况下,使用位运算不仅是提高运算速度,且位掩码能简洁和清晰地表示出二进制变量之间的关系。二进制变量虽然有优势,但是缺点也很明显:不够直观,扩展性不好(在 js 当中的二进制变量,除去符号位,最多只能使用 31 位,当变量的数量超过 31 位就需要组合,此时就会变得复杂)。在阅读源码时,我们需要了解二进制变量和位掩码的使用;但在实际开发中,需要视情况而定,不能盲目使用。

继续深入本仓库的相关章节,可以形成完整的知识闭环:

  • 优先级管理:LanePriority、SchedulerPriority、ReactPriorityLevel三套优先级体系的完整梳理;
  • reconciler 运作流程:scheduleUpdateOnFiber在输入链路中的位置与调用链;
  • fiber 树构造(基础准备):executionContext八种状态、requestUpdateLane、getNextLanes与beginWorkbailout 的完整源码;
  • 栈操作:executionContext与函数调用栈配合维护状态的技巧(batchedUpdates/unbatchedUpdates);
  • fiber 树构造(初次创建) 与 fiber 树构造(对比更新):车道模型在实际 fiber 构造中的落地。
  • 教程
  • 前端

【免费下载链接】react-illustration-series

图解react源码, 用大量配图的方式, 致力于将react原理表述清楚.

项目地址:https://gitcode.com/gh_mirrors/re/react-illustration-series
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询