系列文章:
1 写在前面
React 源码里的概念实在是太多了,以至于如果真的要能完全理解源码的话,我们就不得不提前了解一部分知识,不然看源码的时候完全就是抓瞎。
2 Fiber
2.1 为什么要有 Fiber
想象一下,如果你手头上的事情分别有:
- 带娃👶(主线任务)
- 打游戏🎮(支线任务)
每次你在打游戏🎮要花不少时间,一局游戏不打完就算👶哭了也不能中断,👶非常伤心(我这不争气的爹)。
虽然这是一个不恰当的比喻,但是这就是 React 15 的渲染体验:
- 任务一旦开始,无法中断
- UI 更新被卡住,用户体验差
- 无法分配不同优先级的任务
在 Fiber 架构下,带娃👶和打游戏🎮这两件事就变成:
- 时间分片:在带娃👶的时候如果有空闲时间那么我就可以打一会游戏
- 任务可中断:一旦娃👶哭了,那么我就立马暂停手中的游戏(去带娃👶)
- 任务可恢复:娃👶不哭了,那么我又可以重新在暂停的地方开始游戏🎮啦
- Lane 模型:划分优先级,带娃👶这件事情上优先级是 No.1,其他事情先靠边站🙄
2.2 概念
回归正题,Fiber 它有两个含义:
-
数据结构
- 本质是一个 JavaScript 对象
- 每个组件实例对应一个 Fiber 节点
- 保存组件的类型、props、state、副作用等信息
-
执行单元
- React 把一次渲染工作拆分成很多小的 Fiber 任务(单元)
- 这些单元可以分批执行、中途暂停、恢复、甚至丢弃
2.3 常见属性及说明
Fiber 节点里的属性如下:
| 属性名 | 类型说明 | 描述 |
|---|---|---|
tag | number | 节点类型,如 FunctionComponent、ClassComponent 等。 |
key | string 或 null | 用于 diff 的唯一标识。 |
elementType | 任意 | JSX 转换后的原始类型,如函数、类、'div' 等。 |
type | 任意 | 节点对应的组件类型或原始标签名。 |
stateNode | 对象或 null | 对于 DOM 节点是真实 DOM,对于 class 是实例,函数组件为 null。 |
return | Fiber 或 null | 指向父节点。 |
child | Fiber 或 null | 第一个子节点。 |
sibling | Fiber 或 null | 下一个兄弟节点。 |
index | number | 在兄弟节点中的位置索引。 |
ref | ref 对象或 null | 组件 ref。 |
pendingProps | 任意 | 本次渲染传入的新 props。 |
memoizedProps | 任意 | 上一次渲染的 props。 |
memoizedState | 任意 | 上一次渲染的 state 或 hooks。 |
updateQueue | 对象或 null | setState 等更新队列。 |
dependencies | 对象或 null | 记录当前组件使用的 context。 |
mode | number | Fiber 的模式标志,例如是否启用 ConcurrentMode。 |
flags | 位掩码 | 当前节点本身的副作用标志。 |
subtreeFlags | 位掩码 | 子树副作用集合。 |
deletions | Fiber[] 或 null | 待删除的子节点列表。 |
lanes | 位掩码 | 当前节点的优先级集合。 |
childLanes | 位掩码 | 子树中包含的优先级合集。 |
alternate | Fiber 或 null | 另一份 Fiber 节点(workInProgress)。 |
actualDuration | number | Profiler 记录的渲染耗时。 |
actualStartTime | number | Profiler 渲染开始时间。 |
selfBaseDuration | number | 自身渲染耗时。 |
treeBaseDuration | number | 子树渲染总耗时。 |
_debugOwner | Fiber 或 null | DEV 环境用于调试的父组件。 |
_debugSource | 对象或 null | DEV 环境的源码位置信息。 |
2.3 Fiber 树
Fiber 树就是以 Fiber 节点为最小单位组织成的树结构,它用来描述 React 里的页面结构:
graph TD
%% 主 Fiber 树节点
A["Fiber A"]
B["Fiber B"]
C["Fiber C"]
D["Fiber D"]
%% alternate Fiber 节点
A2["Fiber A'"]
B2["Fiber B'"]
%% child 关系
A -->|child| B
B -->|child| C
%% sibling 关系
C -->|sibling| D
%% return 关系
B -->|return| A
C -->|return| B
D -->|return| B
%% alternate 关系
A ---|alternate| A2
B ---|alternate| B2
3 Lane
3.1 概念
Lane 是 React 内部用于调度系统的一种优先级管理机制。每个 Lane 是一个二进制位(bit),多个 Lane 可以通过按位或(|)组合成一个 bitmask,从而实现多任务的并行调度与优先级控制。不同类型的更新任务(如同步更新、过渡动画、输入事件等)会被分配到不同的 Lane,React 会根据这些 Lane 的优先级来决定执行顺序。
| Lane名称 | 二进制值 | 十进制值 | 说明 |
|---|---|---|---|
| SyncLane | 0b000...0001 | 1 | 同步更新(最高优先级,如 setState in flushSync) |
| InputContinuousLane | 0b000...0010 | 2 | 连续输入事件(如拖动、滚动) |
| DefaultLane | 0b000...0100 | 4 | 默认更新优先级 |
| TransitionLanes | 0b000...1xxx | 8 ~ 0x800000 | 各类 transition,支持并发过渡,如 startTransition |
| TransitionLane1 | 0b000...1000 | 8 | 第一个 transition lane |
| TransitionLane2 | 0b000...1_0000 | 16 | 第二个 transition lane |
| ...(共 16 个) | ... | ... | 一直延续到 TransitionLane16(1 << 21) |
| RetryLanes | ... | 1 << 22 ~ 1 << 23 | Suspense retry 后的更新 |
| IdleLane | 0b100...0000 | 1073741824 | 最低优先级,后台/预渲染任务 |
| OffscreenLane | 1 << 29 | 536870912 | 用于隐藏组件的更新(如 <Offscreen />) |
相应的,React 里也将事件分门别类并且分别赋予了不同的优先级:
| 事件优先级名称 | 对应的 Lane | 常见对应事件类型(事件名) | 描述 |
|---|---|---|---|
NoEventPriority | NoLane | 无(无任务) | 没有任务,空闲状态 |
DiscreteEventPriority | SyncLane | 点击(click)、键盘按键(keydown)、提交(submit)等离散事件 | 最高优先级,立即响应用户操作 |
ContinuousEventPriority | InputContinuousLane | 鼠标拖拽(mousemove)、滚动(scroll)、连续按键(keypress)等连续输入事件 | 用户持续输入,保持流畅体验 |
DefaultEventPriority | DefaultLane | 定时器回调、异步请求完成等默认优先级事件 | 默认普通优先级 |
IdleEventPriority | IdleLane | 空闲回调、后台任务 | 低优先级,仅在主线程空闲时执行 |
3.2 相关方法
要理解源码里关于 Lane 操作的方法,首先就要理解 Lanes 和 Lane 的区别。
本质上 Lane 和 Lanes 都是 number,但是 Lane 用来单独表示一个优先级,而 Lanes 则用来表示多个优先级。
| 对比项 | Lane | Lanes |
|---|---|---|
| 定义 | 单个优先级标识,表示一个更新任务的优先级位。 | 多个优先级的组合,表示当前包含哪些优先级的更新。 |
| 类型 | number(通常是 2^n,即 0b000...1 形式) | number(多个 Lane 的按位或运算结果) |
| 二进制形式 | 仅一个位为 1,例如:0b000000000000000000000010 | 多个位可以为 1,例如:0b000000000000000000000110 |
| 作用 | 表示某一个具体的更新优先级 | 表示当前任务涉及的所有优先级 |
| 用途举例 | getHighestPriorityLane(lanes): Lane | workInProgressRootRenderLanes: Lanes |
| 常用操作 | 单独判断、与 Lanes 进行比较等 | 位运算(如合并:mergeLanes(a, b),判断:includesSomeLane(lanes, lane)) |
| 使用场景 | 表示当前某个任务的目标优先级 | 表示当前某个 Fiber 或 Root 上所有挂起的任务优先级 |
| 是否复合类型 | 否(仅代表一个 bit) | 是(可能包含多个 bit) |
3.2.1 getHighestPriorityLane
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
这个函数是用来获取 Lanes 里最高优先级的 Lane 来优先进行处理。
在二进制运算里:-lanes = ~lanes + 1,因此 lanes & -lanes 就相当于 lanes & (~lanes + 1),所以我们可以拿到 lanes 最右边的 1。
在 Lane 的设计里,优先级是从左到右递增的,因此最右边的 Lane 优先级就是最高的。
3.2.2 mergeLanes
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
尽管 React 里有优先级控制,但是往往同一个事件会有多个优先级叠加,这时候就需要将优先级提上来了,而 mergeLanes 是用来合并 2 个不同的优先级的。
举例🌰:对于 001 和 010 这两个优先级,合并之后就变成了 011。
3.2.3 removeLanes
removeLanes 顾名思义就是将一个子 lanes 从 Lanes 里移除掉。
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
源码里这里比较好理解,就是直接 set & ~subset,这样返回的 Lanes 里就不包含 subset 了。
3.2.4 intersectLanes
intersectLanes 就是取 a 和 b 2 个 Lanes 的交集
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}
因为 Lane 是用二进制位的 1 来表示的,所以说用 & 操作之后,对应二进制位上没有 1 的部分都会被去掉。
3.2.5 isSubsetOfLanes
判断目标 Lane 是否为 Lanes 的子集。
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
return (set & subset) === subset;
}
简单理解:如果 set & subset 的结果是 subset 的话,说明 set 和 subset 的交集是 subset,也就是说 subset 是 set 的子集。
3.2.6 includeSomeLane
判断目标 Lanes 是否包含 Lane:
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane): boolean {
return (a & b) !== NoLanes;
}
这个函数只判断是否有交集,和上面的 intersectLanes 方法有些类似,但是返回的是 boolean。
3.2.7 higherPriorityLane
返回优先级更高的 Lane:
export function higherPriorityLane(a: Lane, b: Lane): Lane {
// This works because the bit ranges decrease in priority as you go left.
return a !== NoLane && a < b ? a : b;
}
因为 Lane 是 bitmask 的设计,低位优先级更高,所以这里判断优先级的方法是判断大小,数字越小优先级越高。
3.2.8 pickArbitraryLaneIndex
function pickArbitraryLaneIndex(lanes: Lanes) {
return 31 - clz32(lanes);
}
clz32 是 JS 内置函数,全称 Count Leading Zeros in 32-bit integer。
它会返回 32 位无符号整数开头有多少个连续的 0。
因此 pickArbitraryLaneIndex 就是返回对应 Lanes 二进制位里 1 的最高位,可以理解为拿到 Lanes 里优先级最低的 Lane 的 index。
3.2.9 getLanesOfEqualOrHigherPriority
获取一个大于等于目标 Lanes 的 Lane:
function getLanesOfEqualOrHigherPriority(lanes: Lane | Lanes): Lanes {
// Create a mask with all bits to the right or same as the highest bit.
// So if lanes is 0b100, the result would be 0b111.
// If lanes is 0b101, the result would be 0b111.
const lowestPriorityLaneIndex = 31 - clz32(lanes);
return (1 << (lowestPriorityLaneIndex + 1)) - 1;
}
前面提到 31 - clz32(lanes) 其实就是获取 bitmask 里最左位的 1 的 index。
因此,如果 lanes 为 0b100,那么 lowestPriorityLaneIndex + 1 则为 3。
所以 1 << (lowestPriorityLaneIndex + 1) 则为 0b1000,减掉 1 之后返回的值为 0b0111。
这样一来,我们就可以通过这个函数来拿到“优先级相同或更高”的所有 lanes。
4 总结
在这篇文章里,我们对 Fiber架构和 Lane模型都有了初步的了解,对于后续继续深入阅读 React 源码帮助非常大。