react17源码中部分二进制计算的解释

寻技术 React 2023年11月20日 118
theme: qklhk-chocolate
highlight: a11y-dark

react17放弃了之前的expirationTime而启用了lane模型,故而在原来16的基础上又产生了更多的二进制运算,在接下来的一段时间我打算把这些二进制运算都整明白了、

关于react为什么会启用lane模型的官方解释
js中的二进制位运算都是以32位补码的形式计算的,更多解释可以参考mdn

1.关于上下文的切换

在react的更新中,executionContext按照字面意思即为执行上下文,executionContext的默认值是0NoContext, 此后的executionContext的更新中,都是与其他不同的上下文以按位或的运算的方式进行更新的,react17里的不同上下文有如下8种:

var NoContext =
/*             */
0;
var BatchedContext =
/*               */
1;
var EventContext =
/*                 */
2;
var DiscreteEventContext =
/*         */
4;
var LegacyUnbatchedContext =
/*       */
8;
var RenderContext =
/*                */
16;
var CommitContext =
/*                */
32;
var RetryAfterError =
/*       */
64;

分析位运算可能写出他们的二进制形式更加清晰一点,由上到下依次为:

NoContext              0000000
BatchedContext         0000001
EventContext           0000010
DiscreteEventContext   0000100
LegacyUnbatchedContext 0001000
RenderContext          0010000
CommitContext          0100000
RetryAfterError        1000000

实际参与计算时应该是32位的但是这里取7位是因为2^6是64多余的0对于我的分析来说是没有意义的。
react17一共在11个地方对context进行了|=方式的更新,不同的更新方式对应了不同的context的变量,比方,batchedUpdates的更新为executionContext |= BatchedContext;,unbatchedUpdates中的更新则相应的为executionContext |= LegacyUnbatchedContext

这些更新在其他地方大同小异,基本都是|=方式进行更新, 但在unbatchedUpdates更新前,却进行了一次这个操作:executionContext &= ~BatchedContext,这里我们先不管这段代码具体是什么作用,我们暂且讨论一下这个二进制运算会产生何种效果。首先假设在某一状态时 executionContext 与 BatchedContext 发生了一次运算:

 executionContext | BatchedContext
 0000000          | 0000001
 executionContext	= 	0000001	   

那么 executionContext &= ~BatchedContext:

	executionContext & ~BatchedContext
	0000001          &  1111110
	executionContext =  0000000

可以看到executionContext &= ~BatchedContext的效果其实就是还原了上次executionContext |= BatchedContext;前的executionContext。这个其实很好理解,要知道所有的context他在以上的7位二进制中只占了其中的一位,那么无论之前的executionContext与其中那个context进行按位或|运算,其结果就是只是让executionContext的某个位为1,拿BatchedContext举例他只是使得executionContext右边的第一位为1,而在按位取反之后,除了自己所占的那个位为0其余都成了1,再与executionContext进行按位与&运算,则executionContext中其他位为1的(也就是说executionContext可能与其他上下文也一起进行了|)是不会变得,只有~BatchedContext对应的那一位现在是0,因此不管executionContext中这个位置的数字是0还是1其结果最后都为0,也就是上边说的executionContext还原到了与BatchedContext|=前的状态。这条准则换成其他任何一个context都是一样的。

2.关于上下文的判断

拿一个地方举例:

if (!((executionContext & (RenderContext | CommitContext)) === NoContext)) {
    {
      throw Error( "Should not already be working." );
    }
  } 

这个if里的表达式为真,即 左边的要不等于NoContext,左边的不等于NoContext则说明executionContext里属于RenderContext或CommitContext的那个位为1,而那个位为1就说明 executionContext肯定与其中之一发生了按位或运算,而发生按位或也就代表着某个地方的代码执行过了。以上。

小结: 其实以上这种二进制的运算,应该属于二进制掩码的应用,二进制的运算除了数学上的特征比方左移右移相当于乘以2除以2,其他方面更觉得像一种图形一样的运算,因为 & | 这两种运算是不会产生进位的,因此看源码的二进制运算更多的应该从类似图形变换的角度去理解每个二进制运算的含义,而不是数字之间的运算。

3.关于lane

关于lane有篇文章个人决得讲的特别好,推荐一看

关于lane的基本解释

react17中使用了31位二进制来表示lane的概念,其中31位中占一位的变量称作lane,占据多位的称为lanes,react17中全部lane如下(二进制形式):

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;
关于lane的基本使用
1. 创建fiber

每一个fiber创建的时候其lanes,childLanes字段都被初始化为NoLanes

2. 创建update

react中无论是初始的渲染,还是setstate或者由hooks派发出来的更新操作,都会调用createupdate方法创建一个update对象,不同之处是,对于更新时的update对象来说lane字段是什么,是由与之相关的fiber的mode字段决定的:

...
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
...
function requestUpdateLane(fiber) {
  ...
  
  var mode = fiber.mode;

  if ((mode & BlockingMode) === NoMode) {
    return SyncLane;
  } else if ((mode & ConcurrentMode) === NoMode) {
    return getCurrentPriorityLevel() === ImmediatePriority$1 ? SyncLane : SyncBatchedLane;
  } 
  ...
}

mode一般来说有如下几种:

var NoMode = 0;
var StrictMode = 1;

var BlockingMode = 2;
var ConcurrentMode = 4;
var ProfileMode = 8;
var DebugTracingMode = 16;

HostRootFiber(整个react应用的初始fiber节点)初始化的时候,目前来看其tagLegacyRoot,在createHostRootFiber方法中赋予其mode:

if (tag === ConcurrentRoot) {
  mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
  mode = BlockingMode | StrictMode;
} else {
  mode = NoMode;
}

由其tag可以知道HostRootFiber的mode即为noMode,之后在beginwork开始创建各个子节点的fiber时,其fiber的mode直接继承自父节点:

      var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);

因此 对于大部分fiber来说,在一次更新中由其派发的update的lane是SyncLane。

3.更新过程中对fiber上各个字段的更新

每个更新时都会自scheduleUpdateOnFiber始,而在scheduleUpdateOnFiber中,会

  1. 更新fiber上的lanes字段:
	sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);

然后沿fiber树向上遍历,更新每个父节点fiber的childLanes字段

while (parent !== null) {
    ...
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    ...
}

其中mergeLanes 就是将两个变量进行按位或运算,产生新的lanes。 即由此可以看到,当前各种各样的更新的lane最终都会在根节点的childLanes字段上有体现。

  1. 更新root根节点的各个字段
  • pendingLanes:
root.pendingLanes |= updateLane;
  • suspendedLanes,pingedLanes
var higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111
root.suspendedLanes &= higherPriorityLanes;
root.pingedLanes &= higherPriorityLanes;

几句解释: higherPriorityLanes - 1 ,比方代码中的注释Turns 0b1000 into 0b0111,他假如是一个lanes字段,那么他的值就是比当前updateLane的优先级更高的各个lane按位或之后的结果,因为结合前边各个lane的值可以看到,越靠近右边的1的位置的优先级越高, 至于suspendedLanes,pingedLanes的更新就是保留了比当前优先级更高的lane。

  • eventTimes
    这是一个31长度的数组,每一位对应一个lane
var eventTimes = root.eventTimes;
var index = laneToIndex(updateLane); // 获取当前lane在eventTimes的数组索引
// We can always overwrite an existing timestamp because we prefer the most
/ recent event, and we assume time is monotonically increasing.
eventTimes[index] = eventTime;//eventTime是创建当前update的时间

未完待续。。。

  • 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
关闭

用微信“扫一扫”