Lanes 模型
起因、需求
在react并发模式中,会有很多对优先级、批量更新进行判断的场景,比如:
- 过期任务或者同步任务使用
同步优先级(最高优) - 用户交互产生的更新(比如点击事件)使用高优先级
- 网络请求产生的更新使用一般优先级
Suspense使用低优先级- 需要判断:某个 update 是否可以放在某次批量更新中进行更新?
- 从批量更新中,删除属于某个优先级的任务
React需要设计一套满足如下需要的优先级机制:
-
可以表示
优先级的不同 -
可能同时存在几个同
优先级的更新,所以还得能表示批的概念 -
方便进行
优先级相关计算
lanes模型借鉴了同样的概念,使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级。
React 对 lanes 模型的定义
Lane 和 Lanes
想象你身处赛车场。
不同的赛车疾驰在不同的赛道。内圈的赛道总长度更短,外圈更长。某几个临近的赛道的长度可以看作差不多长。
lanes模型借鉴了同样的概念,使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级。
Lane 指代一条具体的赛道,通常表示单个更新的优先级,比如 setState 的优先级。lane 只有一位比特是 1 的 31 位二进制数
// react 中优先级最高的赛道: SyncLane
export const SyncLane: Lane = 0b0000000000000000000000000000001;
在前面起因、需求部分提到:可能同时存在几个同优先级的更新,所以还得能表示批的概念
React 用 Lanes 这个词来指代一批lane, lanes 是可能有多位比特是 1 的 31 位二进制数
Lanes 模型的第一个 mr是这样解释的lanes 的
In the new model, we have decoupled those two concepts. Groups of tasks are instead expressed not as relative numbers, but as bitmasks:
在新模型中,我们已经将这两个概念(任务优先级和任务分组)解耦。任务组不再表示为相对数字,而是表示为位掩码:
const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;The type of the bitmask that represents a task is called a
Lane. The type of the bitmask that represents a batch is calledLanes. (Note: these names are not final. I know using a plural is a bit confusing, but since the name appears all over the place, I wanted something that was short. But I'm open to suggestions.)表示任务的位掩码类型称为 “车道”(Lane)。表示批次的位掩码类型称为 “车道组”(Lanes)。(注意:这些名称并非最终确定。我知道使用复数形式有点令人困惑,但由于这个名称在各处都有出现,我希望它简短一些。不过我也欢迎大家提出建议。)
React 中 定义的lane、lanes 常量:
Lane 表示单个更新的优先级,比如 setState 的优先级。lane 都只有一位比特是 1
Lanes 表示 “batch”(批次?)的优先级,比如 concurrent 模式中的批量更新。 lanes 会有多位比特位 1
// 从存储的角度来说,lane 和 lanes 都是 31 位二进制,没什么区别
// 0 代表没有优先级,没有实际意义, 一般用于辅助判断 lanes 之间是否有交集关系、优先级强弱。
// root.pendingLanes === NoLanes 就说明 root.pendingLanes 中没有要处理的车道。 暂时不必纠结
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;
Lanes 相关的运算
lanes 实际上是31 位二进制数,所以经常会涉及到一些二进制的位运算。下面会放到具体例子中讲解
- 判断某个 lanes 是否包含某个 lane
const isBelongBatch = taskLane & batchLanes !== NoLanes;
NoLanes === 0,本身并不标识任何优先级,一般用于辅助判断 lanes 之间是否有交集关系、优先级强弱等等。比如
root.pendingLanes === NoLanes就说明 root.pendingLanes 中没有要处理的车道。
使用场景:
提交了一个低优先级的任务(比如 useTransication ),而该任务对应的 lane 不在当前 workinProgress正在处理的lanes里,没必要立即处理该低优先级任务,而是先记录在 fiber 上,后续调度对应lanes的任务时再做处理。
比如:
useTransication 对应的赛道: 0b000_0000_0000_0000_0000_0100_0000_0000 是 10 号赛道,
假设当前workinProgress 正在处理: 0b000_0000_0000_0000_0000_0000_0011_1111 是 0-5 号赛道
两者没有交集并且 10 号赛道的优先级低于 0-5 这一批赛道
因此 react 不会立即处理这个新的任务,而是先记录在 fiber 上,后续调度到包含 10 号赛道的批处理时再做处理。
- 哪个lane的优先级高
lane 的数值越小,优先级越高。直接用<比较符就行。
const lane1= 0b00000001000
const lane2= 0b00001000000
lane1<lane2 ===true
所以 lane1 的优先级更高
- 从某 lanes 中,剔除掉另一个 lanes 或 lane
在组件的生命周期里,更新的优先级或许会动态变化。比如,在用户进行交互的时候,某些更新的优先级会提高,而之前的一些低优先级更新可能就不再需要了。这时候就可以通过剔除特定的 lanes 来调整调度。
function removeLaneFromLanes(lanes, lane){
return lanes & ~lane // ~表示按位取反,&表示按位与
}
解释:假设有lanesA和 laneB的值如下
lanesA = 1111110010 //
laneB = 0001100001 //
那 removeLaneFromBatch(lanesA,laneB) 的预期结果为 1110010010
即:lanesA和laneB中都为 1 的比特位在结果中为 0,其余比特位继承 batch
计算过程如下:
~laneA = 1110011110 // 对 lane 按位取反
lanesA & (~laneB) =1110010010 // 符合预期结果 ✅
- 获取某 lanes 里优先级最高的 lane
这个问题可以转化为获取 lanes 这个比特串的「最低有效位所对应的 lane」,也即最右边的 1
const a = 0b0000_0100_1010
const a_最低有效位lane = 0b0000_0000_0010
// ^ 这里
那么怎么计算这个「最低有效位所对应的 lane」呢?
使用 & - 运算:
- 在计算机中,负数是以补码形式存储的。而补码计算方式是原码取反加 1。
- 当我们对 lanes 取负时,会得到其补码。 如 0b010100 的补码是 0b101100
- 然后将 lanes 与其补码进行按位与运算(&),最终结果会保留 lanes 中最右边的 1,其余位都变为 0。
例如,假设lanes为 0b010100(十进制 20),那么 -lanes 为 0b101100(补码形式),
lanes & -lanes 结果为 0b000100(十进制 4)。
react源码中用下面这个函数来获取某 lanes 中的最高优先级 lane
export function getHighestPriorityLane(lanes: Lanes): Lane { return lanes & -lanes; }
- 获取某 lanes 里优先级最低的 lane
类似地,有时也需要获取最低优先级,也即最左边的 1
const lanes = 0b0000_0100_1010
const 优先级最低 lane = 0b0000_0100_0000
// ^ 这里
计算方式与获取最高优先级不太一样,直接看下面函数里的注释
// 找到lanes中优先级最低的那一个lane
function getLowestPriorityLane(lanes: Lanes): Lane {
// This finds the most significant non-zero bit.
/**
* @From MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
* Math.clz32() 函数返回一个数字在转换成 32 无符号整形数字的二进制形式后,开头的 0 的个数
* 比如 0b0000000000000000000000011100000, 开头的
* 0 的个数是 24 个, 则
* Math.clz32(0b0000000000000000000000011100000) 返回 24.
* 1 === 0b0000000000000000000000000000001
* 1 << 24 = 0b0000001000000000000000000000000 // 1 << 24 代表把 1 对应的二进制数整体左移24 位
*
* */
const index = 31 - clz32(lanes);
return index < 0 ?
NoLanes
:
1 << index;
}
在 React 中的实际应用
源码中涉及到 lanes 最多的**getNextLanes** 和 getHighestPriorityLanes ****这两个函数
- getNextLanes
该函数广泛用于react-reconciler,用于从root.pendingLanes中找出优先级最高的「预设车道」
预设车道为上文中列出来的那些,react 里定义的 lean/leans常量
/**
* 该函数从root.pendingLanes中找出优先级最高的「预设车道」
* @param {*} root
* @param {*} wipLanes
* @returns
*/
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
// Early bailout if there's no pending work left.
// 在没有剩余任务的时候,跳出更新
const pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return_highestLanePriority = NoLanePriority;
return NoLanes;
}
let nextLanes = NoLanes;
let nextLanePriority = NoLanePriority;
const expiredLanes = root.expiredLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
// Check if any work has expired.
// 检查是否有更新已经过期
if (expiredLanes !== NoLanes) {
// 已经过期了,就需要把渲染优先级设置为同步,来让更新立即执行
nextLanes = expiredLanes;
nextLanePriority = return_highestLanePriority = SyncLanePriority;
} else {
// Do not work on any idle work until all the non-idle work has finished,
// even if the work is suspended.
// 即使具有优先级的任务被挂起,也不要处理空闲的任务,除非有优先级的任务都被处理完了
// nonIdlePendingLanes 是所有需要处理的优先级。然后判断这些优先级
// (nonIdlePendingLanes)是不是为空。
//
// 不为空的话,把被挂起任务的优先级踢出去,只剩下那些真正待处理的任务的优先级集合。
// 然后从这些优先级里找出最紧急的return出去。如果已经将挂起任务优先级踢出了之后还是
// 为空,那么就说明需要处理这些被挂起的任务了。将它们重启。pingedLanes是那些被挂起
// 任务的优先级
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
// nonIdleUnblockedLanes也就是未被阻塞的那些lanes,未被阻塞,那就应该去处理。
// 它等于所有未闲置的lanes中除去被挂起的那些lanes。& ~ 相当于删除
if (nonIdleUnblockedLanes !== NoLanes) {
// nonIdleUnblockedLanes不为空,说明如果有任务需要被处理。
// 那么从这些任务中挑出最重要的
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
nextLanePriority = return_highestLanePriority;
} else {
// 如果目前没有任务需要被处理,就从正在那些被挂起的lanes中找到优先级最高的
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
nextLanePriority = return_highestLanePriority;
}
}
} else {
// The only remaining work is Idle.
// 剩下的任务是闲置的任务。unblockedLanes是闲置任务的lanes
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
// 从这些未被阻塞的闲置任务中挑出最重要的
nextLanes = getHighestPriorityLanes(unblockedLanes);
nextLanePriority = return_highestLanePriority;
} else {
if (pingedLanes !== NoLanes) {
// 找到被挂起的那些任务中优先级最高的
nextLanes = getHighestPriorityLanes(pingedLanes);
nextLanePriority = return_highestLanePriority;
}
}
}
}
if (nextLanes === NoLanes) {
// 找了一圈之后发现nextLanes是空的,return一个空
// This should only be reachable if we're suspended
// TODO: Consider warning in this path if a fallback timer is not scheduled.
return NoLanes;
}
// If there are higher priority lanes, we'll include them even if they
// are suspended.
// 如果有更高优先级的lanes,即使它们被挂起,也会放到nextLanes里。
nextLanes = pendingLanes & getEqualOrHigherPriorityLanes(nextLanes);
// nextLanes 实际上是待处理的lanes中优先级较高的那些lanes
// If we're already in the middle of a render, switching lanes will interrupt
// it and we'll lose our progress. We should only do this if the new lanes are
// higher priority.
/*
* 翻译:如果已经在渲染过程中,切换lanes会中断渲染,将会丢失进程。
* 只有在新lanes有更高优先级的情况下,才应该这样做。(只有在高优先级的任务插队时,才会这样做)
*
* 理解:如果正在渲染,但是新任务的优先级不足,那么不管它,继续往下渲染,只有在新的优先级比当前的正在
* 渲染的优先级高的时候,才去打断(高优先级任务插队)
* */
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
// If we already suspended with a delay, then interrupting is fine. Don't
// bother waiting until the root is complete.
(wipLanes & suspendedLanes) === NoLanes
) {
getHighestPriorityLanes(wipLanes);
const wipLanePriority = return_highestLanePriority;
if (nextLanePriority <= wipLanePriority) {
return wipLanes;
} else {
return_highestLanePriority = nextLanePriority;
}
}
// 以下内容暂时未完全理解,翻译仅供参考
// Check for entangled lanes and add them to the batch.
// 检查entangled lanes并把它们加入到批处理中
//
// A lane is said to be entangled with another when it's not allowed to render
// in a batch that does not also include the other lane. Typically we do this
// when multiple updates have the same source, and we only want to respond to
// the most recent event from that source.
/*
* 当一个lane禁止在不包括其他lane的批处理中渲染时,它被称为与另一个lane纠缠在一起。通常,当多个更
* 新具有相同的源时,我们会这样做,并且我们只想响应来自该源的最新事件。
* */
//
// Note that we apply entanglements *after* checking for partial work above.
// This means that if a lane is entangled during an interleaved event while
// it's already rendering, we won't interrupt it. This is intentional, since
// entanglement is usually "best effort": we'll try our best to render the
// lanes in the same batch, but it's not worth throwing out partially
// completed work in order to do it.
//
/*
* 注意,我们在检查了上面的部分工作之后应用了纠缠。
这意味着如果一个lane在交替事件中被纠缠,而它已经被渲染,我们不会中断它。这是有意为之,因为纠缠通常
是“最好的努力”:我们将尽最大努力在同一批中渲染lanes,但不值得为了这样做而放弃部分完成的工作。
* */
// For those exceptions where entanglement is semantically important, like
// useMutableSource, we should ensure that there is no partial work at the
// time we apply the entanglement.
/*
* 对于那些纠缠在语义上很重要的例外,比如useMutableSource,我们应该确保在应用纠缠时没有部分工作。
* */
const entangledLanes = root.entangledLanes;
if (entangledLanes !== NoLanes) {
const entanglements = root.entanglements;
let lanes = nextLanes & entangledLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
nextLanes |= entanglements[index];
lanes &= ~lane;
}
}
return nextLanes;
}
- getHighestPriorityLanes
/**
* 核心功能:从给定 lanes 中找出最高优先级的「预设车道」(上文中列出来的那些)
*/
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
// 相关commit https://github.com/facebook/react/pull/19302 ,推荐阅读
// 按照优先级以此对比,找到了就return
if ((SyncLane & lanes) !== NoLanes) {
// 如果lanes中有同步优先级的任务
return_highestLanePriority = SyncLanePriority; // return_highestLanePriority 是什么?看下面33 行的注释
return SyncLane;
}
if ((SyncBatchedLane & lanes) !== NoLanes) {
// 如果lanes中有批量同步的lane
return_highestLanePriority = SyncBatchedLanePriority;
return SyncBatchedLane;
}
if ((InputDiscreteHydrationLane & lanes) !== NoLanes) {
// 选出lanes中与InputDiscreteLanes重合的非0位
return_highestLanePriority = InputDiscreteHydrationLanePriority;
return InputDiscreteHydrationLane;
}
... // 以下累计类似,都是 if 判断,先省略了
}
// 用于在函数执行过程中存储和传递额外的信息,而不仅仅是通过函数的返回值来传递单个结果。
// `getHighestPriorityLanes` 函数在计算出最高优先级车道的同时,
// 还会将对应的优先级存储在这个特定的变量中,
// 供getNextLanes函数后续使用,这样就实现了 “返回” 多个值的效果。
// 供 getHighestPriorityLanes 和 getNextLanes 函数使用:
let return_highestLanePriority: LanePriority = DefaultLanePriority;
Others
位掩码 bitmask 在 React 中的其他运用
react 中记录执行栈也是用的位掩码
type ExecutionContext = number;
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;
参考资料
github.com/facebook/re… lane最初的 mr ,包含对 lane 模型来源、技术描述