Lane (车道模型)
英文单词
lane
翻译成中文表示"车道, 航道"的意思, 所以很多文章都将Lanes
模型称为车道模型
Lane
模型的源码在ReactFiberLane.js, 源码中大量使用了位运算(有关位运算的讲解,
首先引入作者对Lane
的解释(相应的 pr), 这里简单概括如下:
Lane
类型被定义为二进制变量, 利用了位掩码的特性, 在频繁运算的时候占用内存少, 计算速度快.Lane
和Lanes
就是单数和复数的关系, 代表单个任务的定义为Lane
, 代表多个任务的定义为Lanes
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
- 使用
Lanes
模型相比expirationTime
模型的优势: Lanes
把任务优先级从批量任务中分离出来, 可以更方便的判断单个任务与批量任务的优先级是否重叠.
// 判断: 单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
.
// 从group中删除或增加task
//1. 通过expirationTime实现
// 0) 维护一个链表, 按照单个task的优先级顺序进行插入
// 1) 删除单个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 >= lowestPriorityInRange;
// 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
中), 可以得到如下结论:
- 可以使用的比特位一共有 31 位
- 共定义了18 种车道(Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为
Lane
和Lanes
类型. - 每一种车道(
Lane/Lanes
)都有对应的优先级, 所以源码中定义了 18 种优先级(LanePriority). - 占有低位比特位的
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
位运算的一个使用场景:
比如有一个场景下,会有很多状态常量 A,B,C…,这些状态在整个应用中在一些关键节点中做流程控制,比如:
if(value === A){ // TODO… }
如上判断 value 等于常量A ,那么进入到 if 的条件语句中。 此时是 value 属性是简单的一对一关系,但是实际场景下 value 可能是好几个枚举常量的集合,也就是一对多的关系,那么此时 value 可能同时代表 A 和 B 两个属性。如下图所示:
此时的问题就是如何用一个 value 表示 A 和 B 两个属性的集合。 这个时候位运算就派上用场了,因为可以把一些状态常量用 32 位的二进制来表示(这里也可以用其他进制),比如:
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 ,此时就可以通过 & 来判断。具体实现如下:
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中的运用
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 是如何通过位运算分离出优先级的。
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 就能分离出最高优先级的任务。
const SyncLane = 0b0000000000000000000000000000001
const InputContinuousLane = 0b0000000000000000000000000000100
const lane = SyncLane | InputContinuousLane
console.log( (lane & -lane) === SyncLane ) // true
优先级区别和联系
在源码中, 3 种优先级位于不同的 js 文件, 是相互独立的.
注意:
LanePriority
和SchedulerPriority
从命名上看, 它们代表的是优先级
ReactPriorityLevel
从命名上看, 它代表的是等级
而不是优先级, 它用于衡量LanePriority
和SchedulerPriority
的等级.
LanePriority
LanePriority`: 属于`react-reconciler`包, 定义于`ReactFiberLane.js
export const SyncLanePriority: LanePriority = 15;
export const SyncBatchedLanePriority: LanePriority = 14;
const InputDiscreteHydrationLanePriority: LanePriority = 13;
export const InputDiscreteLanePriority: LanePriority = 12;
// .....
const OffscreenLanePriority: LanePriority = 1;
export const NoLanePriority: LanePriority = 0;
与fiber
构造过程相关的优先级(如fiber.updateQueue
,fiber.lanes
)都使用LanePriority
.
由于本节重点介绍优先级体系以及它们的转换关系, 关于Lane(车道模型)
在fiber树构造
时的具体使用, 在fiber 树构造
章节详细解读.
SchedulerPriority
SchedulerPriority
, 属于scheduler
包, 定义于SchedulerPriorities.js
中
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
与scheduler
调度中心相关的优先级使用SchedulerPriority
.
ReactPriorityLevel
reactPriorityLevel
, 属于react-reconciler
包, 定义于SchedulerWithReactIntegration.js
中
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
// NoPriority is the absence of priority. Also React-only.
export const NoPriority: ReactPriorityLevel = 90;
LanePriority
与SchedulerPriority
通过ReactPriorityLevel
进行转换
转换关系
为了能协同调度中心(scheduler
包)和 fiber 树构造(react-reconciler
包)中对优先级的使用, 则需要转换SchedulerPriority
和LanePriority
, 转换的桥梁正是ReactPriorityLevel
.
在SchedulerWithReactIntegration.js中
, 可以互转SchedulerPriority
和 ReactPriorityLevel
:
// 把 SchedulerPriority 转换成 ReactPriorityLevel
export function getCurrentPriorityLevel(): ReactPriorityLevel {
switch (Scheduler_getCurrentPriorityLevel()) {
case Scheduler_ImmediatePriority:
return ImmediatePriority;
case Scheduler_UserBlockingPriority:
return UserBlockingPriority;
case Scheduler_NormalPriority:
return NormalPriority;
case Scheduler_LowPriority:
return LowPriority;
case Scheduler_IdlePriority:
return IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
// 把 ReactPriorityLevel 转换成 SchedulerPriority
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
switch (reactPriorityLevel) {
case ImmediatePriority:
return Scheduler_ImmediatePriority;
case UserBlockingPriority:
return Scheduler_UserBlockingPriority;
case NormalPriority:
return Scheduler_NormalPriority;
case LowPriority:
return Scheduler_LowPriority;
case IdlePriority:
return Scheduler_IdlePriority;
default:
invariant(false, 'Unknown priority level.');
}
}
在ReactFiberLane.js中
, 可以互转LanePriority
和 ReactPriorityLevel
:
export function schedulerPriorityToLanePriority(
schedulerPriorityLevel: ReactPriorityLevel,
): LanePriority {
switch (schedulerPriorityLevel) {
case ImmediateSchedulerPriority:
return SyncLanePriority;
// ... 省略部分代码
default:
return NoLanePriority;
}
}
export function lanePriorityToSchedulerPriority(
lanePriority: LanePriority,
): ReactPriorityLevel {
switch (lanePriority) {
case SyncLanePriority:
case SyncBatchedLanePriority:
return ImmediateSchedulerPriority;
// ... 省略部分代码
default:
invariant(
false,
'Invalid update priority: %s. This is a bug in React.',
lanePriority,
);
}
}
优先级使用
通过reconciler运行流程中的归纳, reconciler
从输入到输出一共经历了 4 个阶段, 在每个阶段中都会涉及到与优先级
相关的处理. 正是通过优先级
的灵活运用, React
实现了可中断渲染
,时间切片(time slicing)
,异步渲染(suspense)
等特性.
突然很后悔当初没有跟老师好好学数电,在此缅怀许院王教授