【React源码阅读】React 渲染流程 —— 前置知识

203 阅读9分钟

系列文章:

1 写在前面

React 源码里的概念实在是太多了,以至于如果真的要能完全理解源码的话,我们就不得不提前了解一部分知识,不然看源码的时候完全就是抓瞎。

2 Fiber

2.1 为什么要有 Fiber

想象一下,如果你手头上的事情分别有:

  • 带娃👶(主线任务)
  • 打游戏🎮(支线任务)

每次你在打游戏🎮要花不少时间,一局游戏不打完就算👶哭了也不能中断,👶非常伤心(我这不争气的爹)。
虽然这是一个不恰当的比喻,但是这就是 React 15 的渲染体验:

  • 任务一旦开始,无法中断
  • UI 更新被卡住,用户体验差
  • 无法分配不同优先级的任务

Fiber 架构下,带娃👶和打游戏🎮这两件事就变成:

  • 时间分片:在带娃👶的时候如果有空闲时间那么我就可以打一会游戏
  • 任务可中断:一旦娃👶哭了,那么我就立马暂停手中的游戏(去带娃👶)
  • 任务可恢复:娃👶不哭了,那么我又可以重新在暂停的地方开始游戏🎮啦
  • Lane 模型:划分优先级,带娃👶这件事情上优先级是 No.1,其他事情先靠边站🙄

2.2 概念

回归正题,Fiber 它有两个含义:

  1. 数据结构

    • 本质是一个 JavaScript 对象
    • 每个组件实例对应一个 Fiber 节点
    • 保存组件的类型、props、state、副作用等信息
  2. 执行单元

    • React 把一次渲染工作拆分成很多小的 Fiber 任务(单元)
    • 这些单元可以分批执行、中途暂停、恢复、甚至丢弃

2.3 常见属性及说明

Fiber 节点里的属性如下:

属性名类型说明描述
tagnumber节点类型,如 FunctionComponent、ClassComponent 等。
keystring 或 null用于 diff 的唯一标识。
elementType任意JSX 转换后的原始类型,如函数、类、'div' 等。
type任意节点对应的组件类型或原始标签名。
stateNode对象或 null对于 DOM 节点是真实 DOM,对于 class 是实例,函数组件为 null。
returnFiber 或 null指向父节点。
childFiber 或 null第一个子节点。
siblingFiber 或 null下一个兄弟节点。
indexnumber在兄弟节点中的位置索引。
refref 对象或 null组件 ref。
pendingProps任意本次渲染传入的新 props。
memoizedProps任意上一次渲染的 props。
memoizedState任意上一次渲染的 state 或 hooks。
updateQueue对象或 nullsetState 等更新队列。
dependencies对象或 null记录当前组件使用的 context。
modenumberFiber 的模式标志,例如是否启用 ConcurrentMode。
flags位掩码当前节点本身的副作用标志。
subtreeFlags位掩码子树副作用集合。
deletionsFiber[] 或 null待删除的子节点列表。
lanes位掩码当前节点的优先级集合。
childLanes位掩码子树中包含的优先级合集。
alternateFiber 或 null另一份 Fiber 节点(workInProgress)。
actualDurationnumberProfiler 记录的渲染耗时。
actualStartTimenumberProfiler 渲染开始时间。
selfBaseDurationnumber自身渲染耗时。
treeBaseDurationnumber子树渲染总耗时。
_debugOwnerFiber 或 nullDEV 环境用于调试的父组件。
_debugSource对象或 nullDEV 环境的源码位置信息。

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 概念

LaneReact 内部用于调度系统的一种优先级管理机制。每个 Lane 是一个二进制位(bit),多个 Lane 可以通过按位或(|)组合成一个 bitmask,从而实现多任务的并行调度与优先级控制。不同类型的更新任务(如同步更新、过渡动画、输入事件等)会被分配到不同的 Lane,React 会根据这些 Lane 的优先级来决定执行顺序。

Lane名称二进制值十进制值说明
SyncLane0b000...00011同步更新(最高优先级,如 setState in flushSync
InputContinuousLane0b000...00102连续输入事件(如拖动、滚动)
DefaultLane0b000...01004默认更新优先级
TransitionLanes0b000...1xxx8 ~ 0x800000各类 transition,支持并发过渡,如 startTransition
  TransitionLane10b000...10008第一个 transition lane
  TransitionLane20b000...1_000016第二个 transition lane
...(共 16 个)......一直延续到 TransitionLane16(1 << 21)
RetryLanes...1 << 22 ~ 1 << 23Suspense retry 后的更新
IdleLane0b100...00001073741824最低优先级,后台/预渲染任务
OffscreenLane1 << 29536870912用于隐藏组件的更新(如 <Offscreen />

相应的,React 里也将事件分门别类并且分别赋予了不同的优先级:

事件优先级名称对应的 Lane常见对应事件类型(事件名)描述
NoEventPriorityNoLane无(无任务)没有任务,空闲状态
DiscreteEventPrioritySyncLane点击(click)、键盘按键(keydown)、提交(submit)等离散事件最高优先级,立即响应用户操作
ContinuousEventPriorityInputContinuousLane鼠标拖拽(mousemove)、滚动(scroll)、连续按键(keypress)等连续输入事件用户持续输入,保持流畅体验
DefaultEventPriorityDefaultLane定时器回调、异步请求完成等默认优先级事件默认普通优先级
IdleEventPriorityIdleLane空闲回调、后台任务低优先级,仅在主线程空闲时执行

3.2 相关方法

要理解源码里关于 Lane 操作的方法,首先就要理解 LanesLane 的区别。 本质上 LaneLanes 都是 number,但是 Lane 用来单独表示一个优先级,而 Lanes 则用来表示多个优先级。

对比项LaneLanes
定义单个优先级标识,表示一个更新任务的优先级位。多个优先级的组合,表示当前包含哪些优先级的更新。
类型number(通常是 2^n,即 0b000...1 形式)number(多个 Lane 的按位或运算结果)
二进制形式仅一个位为 1,例如:0b000000000000000000000010多个位可以为 1,例如:0b000000000000000000000110
作用表示某一个具体的更新优先级表示当前任务涉及的所有优先级
用途举例getHighestPriorityLane(lanes): LaneworkInProgressRootRenderLanes: 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 个不同的优先级的。
举例🌰:对于 001010 这两个优先级,合并之后就变成了 011

3.2.3 removeLanes

removeLanes 顾名思义就是将一个子 lanesLanes 里移除掉。

export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
  return set & ~subset;
}

源码里这里比较好理解,就是直接 set & ~subset,这样返回的 Lanes 里就不包含 subset 了。

3.2.4 intersectLanes

intersectLanes 就是取 ab 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 的话,说明 setsubset 的交集是 subset,也就是说 subsetset 的子集。

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 里优先级最低的 Laneindex

3.2.9 getLanesOfEqualOrHigherPriority

获取一个大于等于目标 LanesLane

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。 因此,如果 lanes0b100,那么 lowestPriorityLaneIndex + 1 则为 3。 所以 1 << (lowestPriorityLaneIndex + 1) 则为 0b1000,减掉 1 之后返回的值为 0b0111
这样一来,我们就可以通过这个函数来拿到“优先级相同或更高”的所有 lanes

4 总结

在这篇文章里,我们对 Fiber架构和 Lane模型都有了初步的了解,对于后续继续深入阅读 React 源码帮助非常大。