Fiber结构
export type Fiber = {
// 一个“实例”在所有组件版本之间共享。我们可以轻松地将其拆分为一个独立对象,以避免向树的备用版本复制过多内容。
// 目前我们将其放在单个对象上,以最小化初始渲染期间创建的对象数量。
// 标识 fiber 类型的标签。
tag: WorkTag,
// 此子节点的唯一标识符。
key: null | string,
// element.type 的值,用于在协调此子节点时保持其身份。
elementType: any,
// 与此 fiber 关联的已解析的函数/类。
type: any,
// 与此 fiber 关联的本地状态(例如,对于DOM元素是DOM节点,对于类组件是实例)。
stateNode: any,
// 概念上的别名
// parent(父): Instance -> return(返回) 由于我们合并了fiber和实例,所以父节点恰好与返回fiber相同。
// 剩余的字段属于 Fiber
// 处理完此 fiber 后要返回的 Fiber。
// 这实际上是父级,但可能存在多个父级(两个),因此这仅是当前正在处理内容的父级。
// 它在概念上等同于堆栈帧的返回地址。
return: Fiber | null,
// 单链表树结构。
child: Fiber | null, // 第一个子 fiber
sibling: Fiber | null, // 下一个兄弟 fiber
index: number, // 在父fiber子节点列表中的索引
// 上次用于附加此节点的 ref。
// 为了生产环境,我将避免添加owner字段,并将其建模为函数。
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
refCleanup: null | (() => void), // 用于清理 ref 的函数
// 输入是处理此 fiber 时传入的数据。参数。Props。
pendingProps: any, // 本次渲染待处理的 props
memoizedProps: any, // 上次渲染用于创建输出的 props
// 状态更新和回调函数的队列。
updateQueue: mixed,
// 用于创建输出的状态
memoizedState: any,
// 此 fiber 的依赖项(上下文、事件等),如果有的话
dependencies: Dependencies | null,
// 描述此 fiber 及其子树属性的位字段。
// 例如,ConcurrentMode 标志指示子树是否应默认为异步模式。
// 创建 fiber 时,它会继承其父级的模式。
// 创建时可以设置其他标志,但在此之后,该值应在 fiber 的整个生命周期内保持不变,尤其是在创建其子 fiber 之前。
mode: TypeOfMode,
// 副作用(Effect)
flags: Flags, // 记录此 fiber 上需要执行的副作用类型(如 Placement、Update、Deletion)
subtreeFlags: Flags, // 此 fiber 的子树中存在的副作用标志的集合
deletions: Array<Fiber> | null, // 记录此 fiber 子树中需要被删除的子 fiber 数组
// 优先级相关
lanes: Lanes, // 此 fiber 所属的调度车道(优先级)
childLanes: Lanes, // 子 fiber 的调度车道
// 这是一个 Fiber 的复用版本。每个被更新的 fiber 最终都会有一个对应的配对节点。
// 在某些情况下,如果需要节省内存,我们可以清理这些配对节点。
alternate: Fiber | null, // 指向 current 树和 workInProgress 树中对应节点的指针,用于双缓存技术。
// 为当前更新渲染此 Fiber 及其子节点所花费的时间。
// 这告诉我们这棵树利用 sCU 进行记忆化的效果如何。
// 每次渲染时它都会重置为 0,并且仅在我们没有“保释”时更新。
// 仅当 enableProfilerTimer 标志启用时才会设置此字段。
actualDuration?: number,
// 如果此 Fiber 当前正处于“渲染”阶段,
// 这会标记工作开始的时间。
// 仅当 enableProfilerTimer 标志启用时才会设置此字段。
actualStartTime?: number,
// 此 Fiber 最近一次渲染的持续时间。
// 当我们因记忆化目的而“保释”时,不会更新此值。
// 仅当 enableProfilerTimer 标志启用时才会设置此字段。
selfBaseDuration?: number,
// 此 Fiber 所有子代的基本时间总和。
// 此值在“完成”阶段向上冒泡。
// 仅当 enableProfilerTimer 标志启用时才会设置此字段。
treeBaseDuration?: number,
// 概念上的别名
// workInProgress(工作中) : Fiber -> alternate(备用) 用于复用的备用节点恰好与工作中的相同。
// 仅用于 __DEV__ 开发模式
_debugInfo?: ReactDebugInfo | null,
_debugOwner?: ReactComponentInfo | Fiber | null,
_debugStack?: string | Error | null,
_debugTask?: ConsoleTask | null,
_debugNeedsRemount?: boolean,
// 用于验证 Hook 的顺序在渲染之间是否发生改变。
_debugHookTypes?: Array<HookType> | null,
};
1.createRoot
一句话概括:创建root,并标记为根节点,返回root
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App title="合一" />
</StrictMode>
);
createRoot方法内部通过调用createContainer方法创建了一个root对象,又通过markContainerAsRoot方法将root标记为根节点。
export function createRoot(){
//1.创建了一个root对象
const root = createContainer(
container,// document.getElementById("root")!
...
);
// 2.标记为根节点
markContainerAsRoot(root.current, container);
//3.
return new ReactDOMRoot(root);
}
2.createContainer
createContainer方法返回一个createFiberRoot()创建的对象
export function createContainer(){
return createFiberRoot(
containerInfo,
tag,
hydrate,
...
);
}
3.createFiberRoot
1.返回一个通过new FiberRoot创建的对象,2.通过initializeUpdateQueue初始化更新队列
- 通过
new FiberRootNode创建一个root对象 - 通过
createHostRootFiber创建一个当前更新的Fiber树,可以有多个 initialState挂载到未初始化的Fiber上,记录当前的状态- 通过
initializeUpdateQueue初始化更新队列
export function createFiberRoot(){
//1.通过`new FiberRootNode`创建一个root对象
const root = new FiberRootNode(
containerInfo,
tag,
hydrate,
...
);
// 2.通过`createHostRootFiber`创建一个当前更新的那个Fiber
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
// 3.
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
//`initialState`挂载到未初始化的Fiber上,记录当前的状态
uninitializedFiber.memoizedState = initialState;
}
//4 .初始化更新队列
initializeUpdateQueue(uninitializedFiber);
//5.
return root;
createHostRootFiber
return createFiber() ,创建并返回一个 HostRoot 类型的 Fiber 节点。
export function createHostRootFiber(
tag: RootTag,
isStrictMode: boolean,
): Fiber {
let mode;
if (disableLegacyMode || tag === ConcurrentRoot) {
mode = ConcurrentMode; //启用并发特性(如时间切片、可中断渲染、过渡),提高交互与调度的弹性。
if (isStrictMode === true) {
mode |= StrictLegacyMode | StrictEffectsMode;
}
} else {
mode = NoMode; // Legacy 行为,不启用并发与严格增强。
}
if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}
return createFiber(HostRoot, null, null, mode);
}
mode类型
ConcurrentMode:启用并发特性(如时间切片、可中断渲染、过渡),提高交互与调度的弹性。NoMode:Legacy 行为,不启用并发与严格增强。StrictLegacyMode:开发环境下的严格模式旧行为,帮助暴露不安全的副作用与生命周期用法(如重复调用某些生命周期/副作用以检验幂等性)。StrictEffectsMode:严格执行副作用检查与行为一致性(开发环境下可能导致 useEffect 等二次调用,以检测副作用是否安全)。ProfileMode:记录渲染耗时、基线时间等性能数据,使 DevTools 能随时开始采样而不会出现空的基线时间。
createHostRootFiber 给你“树的根节点(Fiber)”,负责承载应用元素状态与参与遍历。
createFiberRoot 给你“调度与容器(FiberRoot)”,负责连接宿主环境、管理优先级与渲染生命周期。
- 两者通过 root.current 和 hostRootFiber.stateNode 形成双向关联:FiberRoot 管“何时渲染”,HostRoot Fiber 管“渲染什么”。
initializeUpdateQueue
fiber.updateQueue = queue,初始化根节点的updateQueue
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};
//关键
fiber.updateQueue = queue;
}
render
createRoot(document.getElementById("root")!).render(<App/>)
createRoot返回一个对象,render方法是挂载在这个对象的原型上的,内部主要调用了updateContainer
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function(){
updateContainer(children, root, null, null);
}
updateContainer
- 通过
requestUpdateLane创建lan模型并返回 - 调用
updateContainerImpl方法
export function updateContainer(){
//1.通过`requestUpdateLane`创建lan模型
const lane = requestUpdateLane(current);
//2.
updateContainerImpl(
current,
lane,
...
);
//3.
return lane;
}
requestUpdateLane(current)
React 的 Lane 模型通过一个 31 位的二进制数来定义优先级,数值越小(二进制中低位为 1)的 Lane 优先级越高。下面这个表格能帮你快速了解所有预定义的 Lane 及其用途。
| 优先级类别 | 代表性 Lane 常量 | 二进制值 (示例) | 适用场景 |
|---|---|---|---|
| 最高优先级 (同步) | SyncLane | 0b0000000000000000000000000000001 | 必须同步执行的紧急更新,如用户输入、点击等直接影响交互的反馈。 |
| 高优先级 (用户阻塞) | InputContinuousLane | 0b0000000000000000000000000000100 | 连续的、不应阻塞但需及时响应的用户交互,如滚动、拖动。 |
| 默认优先级 | DefaultLane | 0b0000000000000000000000000010000 | 最常见的普通状态更新,如网络请求返回后更新数据。 |
| 过渡优先级 | TransitionLanes(共16条) | 0b0000000000000000000000001000000(示例) | 非紧急的界面过渡更新,如页面视图切换,可使用 startTransition或 useTransition标记。 |
| 低优先级 | RetryLanes, OffscreenLane | 例如 0b0000000010000000000000000000000 | 重试任务或离屏(尚未显示)内容的预渲染。 |
| 空闲优先级 | IdleLane | 0b0100000000000000000000000000000 | 完全非紧急的任务,仅在浏览器空闲时执行,如后台数据同步。 |
💡 理解 Lane 的运作方式
- 位的含义:你可以将这 31 位想象成 31 条并行赛道。一位上的
1表示一个任务占用了该条车道。一个任务可以占用一条车道(一个单一的Lane),也可以同时占用多条车道(一个Lanes集合)。 - 优先级比较:由于数值越小优先级越高,
SyncLane(二进制最低位为1)拥有最高的优先级。React 内部通过高效的位运算(如按位与&、按位或|)来比较、合并或筛选不同优先级的任务,这比传统的数值比较要快得多 。 - 抢占式调度:高优先级的任务可以中断(抢占)正在执行的低优先级任务。例如,当用户点击按钮(高优先级)时,正在进行的页面过渡渲染(低优先级)会被中断,以确保界面能立即响应用户操作 。
updateContainerImpl
- 通过
createUpdate创建update更新对象 - 通过
enqueueUpdate将update对象,追加到目标 Fiber 的更新队列中,通过并发入队把 lane 标到 fiber.lanes 以及 alternate.lanes。
this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。
this.alternate = NoLanes; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元
-
创建 Update 对象 :
const update = createUpdate(lane)会创建一个 Update 对象。它本质上是一个包含更新信息的 JavaScript 对象,最核心的属性是:lane: 这次更新的优先级。payload: 更新的内容(比如 setState 的新 state)。next: 一个指针,用于将多个 Update 对象链接起来。
-
放入 Update Queue : 紧接着,
enqueueUpdate函数会将这个update对象添加到一个叫做updateQueue的队列中。这个队列存在于每个需要更新的 Fiber 节点(比如类组件或 HostRoot)的 updateQueue 属性上。
数据结构:一个环形链表
updateQueue 并不是一个真正的数组,而是一个 环形单向链表 。可以把它想象成一串用绳子串起来的珠子,并且把最后一颗珠子的绳头系在了第一颗珠子上。
updateQueue.pending: 这个属性是指向链表中最后一个 update对象的指针。- 链接方式 : 每个 update 对象通过它的
next属性指向链表中的下一个 update 。因为是环形的,所以最后一个 update 的 next 会指向第一个 update 。
为什么用环形链表?
- 高效添加 (O(1)) :当有新的更新进来时,只需要操作最后一个节点和新节点,就能把新更新插入到链表末尾,非常快。
- 方便遍历 : 从
queue.pending.next开始,就可以遍历整个链表,处理所有待处理的更新。
我们来详细拆解一下这个 O(1) 的“魔法”
想象一下,你有一串首尾相连的玩具火车车厢(环形链表),你只抓住 最后一节 车厢( last 指针,在 React 中是 updateQueue.pending )。
现在,你想在队尾再加一节新的车厢( newNode )。
如果是一条普通的、不连成环的火车(普通链表),并且你只抓住了第一节车厢:
你必须从第一节车厢开始,一节一节地走到最后,才能把新车厢挂上去。如果火车有100节,你就得走99步。这就是 O(n) 复杂度,效率很低。
但现在我们是环形的,并且抓住了最后一节,情况就完全不同了:
假设现在的火车是 ... -> A -> B ,其中 B 是你抓着的最后一节。因为是环形,所以 B 的下一节其实是头车 A 。
现在,新的车厢 C 来了。我们要把它变成新的最后一节。操作如下:
- 第一步:把新车厢 C 连接到头车 A 。 怎么找到头车 A ?很简单,它就是当前最后一节车厢 B 的下一节。
所以,我们让
C.next = B.next。 现在 C 指向了 A 。 - 第二步:把旧的最后一节 B 连接到新车厢 C 。 我们让
B.next = C。 现在 B 指向了 C , C 指向了 A ,环路接上了! - 第三步:更新你手中的“最后一节” 。
现在 C 才是真正的最后一节了,所以我们更新指针,让你抓住 C 。
last = C。 你看,整个过程只有这固定的几步操作。无论你原来的火车有3节还是300万节,添加新车厢的动作是完全一样的, 根本不需要从头走到尾 。
这就是 O(1) 或“常数时间复杂度” 的含义:操作的耗时与列表的长度无关,永远是那么快。React 用这种巧妙的数据结构来保证向更新队列中添加新任务时的高效率。
总结一下 :
createUpdate 创建了一个“更新包裹”( update 对象),然后 enqueueUpdate 把它挂到了 Fiber 上的 updateQueue 这条“更新流水线”(环形链表)的末尾,等待后续处理。
- 调和调度更新队列
scheduleUpdateOnFiber
function updateContainerImpl(){
//1.
const update = createUpdate(lane);
//2.
const root = enqueueUpdate(rootFiber, update, lane);//通过并发入队把 lane 标到目标 Fiber( fiber.lanes 以及 alternate.lanes )
//3.
if (root !== null) {
scheduleUpdateOnFiber(root, rootFiber, lane);
}
}
scheduleUpdateOnFiber
scheduleUpdateOnFiber 函数,其核心职责就是扮演一个“通知者”和“标记员”的角色,确保更新的优先级( lane )被正确地记录在全局的“任务板”( root.pendingLanes )上,以便调度器能够看到并处理它。
这个过程非常像 医院的急诊分诊系统 :
- 病人到达 (产生更新) :
你调用
setState,就像一个病人到达了医院急诊室。 - 分诊台护士评估 (获取 Lane) :
分诊台的护士(
requestUpdateLane)会根据你的病情(比如是心梗还是普通感冒)给你一个紧急程度的标签,比如“危重”、“紧急”、“普通”。这个标签就是lane。 - 进入等候区 (更新入队) :
你拿着标签被引导到相应的等候区(
enqueueUpdate),和同样病情的其他病人一起等待。 - 通知总调度 (调用
scheduleUpdateOnFiber) : 分诊护士会通知当班的护士长(总调度 Scheduler ):“来新病人了,已经分好级了!” - 在中央白板上登记 (在 Root 上标记) :
护士长(
scheduleUpdateOnFiber)走到医院大厅的中央白板(FiberRoot)前,在“待处理”一栏(pendingLanes)里,把这个病人的紧急级别( lane )登记上去。 这是关键一步 ,现在整个医院都知道有这个级别的病人需要处理。 - 医生接诊 (开始渲染) :
护士长看着白板上的所有登记,决定优先处理哪个级别的病人(比如永远先处理“危重”的),然后派出医生(开始渲染工作)去接诊。
所以,
scheduleUpdateOnFiber就好比是那个确保病人信息被准确登记到中央调度系统的关键角色,没有它,医生就不知道该去处理哪个病人了。 好的,我们用一个最常见的 setState 来完整地走一遍流程。
举例说明
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 就是这里!
setCount(count + 1);
};
return <button onClick={handleClick}>{count}</button>;
}
当你点击这个按钮时,React 内部会发生以下故事:
第 1 步:分配“加急”标签 (获取 Lane)
- 你点击了按钮,这是一个用户交互事件,React 认为它很重要,需要立即响应。
- React 调用
requestUpdateLane,因为它是一个离散的用户输入(如点击),所以会分配一个高优先级的 lane ,比如InputContinuousLane。这相当于给这个更新任务贴上了一个“加急”的标签。 第 2 步:打包更新 (创建 Update) - React 调用
createUpdate,创建一个update 对象。这个对象就像一个快递包裹:
| 标题 | |
|---|---|
包裹内容 ( payload ) | count + 1 这个计算函数。 |
快递单 ( lane ) | InputContinuousLane (加急标签) |
下一个包裹的地址 ( next ) | null (暂时没有下一个) |
第 3 步:放入组件的“待办仓库” (入队 UpdateQueue)
- React 找到 Counter 组件对应的 Fiber 节点。
- 它把这个“快递包裹” ( update 对象) 放入这个 Fiber 节点的
updateQueue(环形链表)的末尾。这个操作非常快,就是我们之前讨论的 O(1) 操作。
第 4 步:拉响“调度铃” (调用 scheduleUpdateOnFiber )
- 包裹放好了,React 必须通知总调度中心有新任务了。于是它调用
scheduleUpdateOnFiber。
第 5 步:在总调度板上登记 (标记 Root)
scheduleUpdateOnFiber内部立即调用markRootUpdated。- 它在整个应用的 FiberRoot 上,找到
pendingLanes这个“总调度板”,然后把InputContinuousLane这个“加急”标签画上去。现在,调度中心知道有一个高优先级的任务需要处理。
第 6 步:调度员出动 (Scheduler 开始工作)
ensureRootIsScheduled函数会确保调度员(Scheduler)被唤醒。- 调度员的
workLoop开始运转,它查看“总调度板” (pendingLanes),发现了一个高优先级的InputContinuousLane任务。 - 它说:“这个任务很急,我得马上处理!” 于是,React 开始从根节点进行渲染工作。
第 7 步:处理“待办仓库” (处理 UpdateQueue)
- 当渲染工作进行到 Counter 组件时,React 会调用
processUpdateQueue来处理它“待办仓库”里的更新。 - 它遍历
updateQueue链表,看到了我们之前放进去的那个“包裹”。 - 它检查包裹上的“加急”标签 (
InputContinuousLane),发现和当前正在处理的优先级匹配。 - 于是,它打开包裹,执行 count + 1 这个计算,得到新的
state { count: 1 }。
第 8 步:更新界面 (Commit)
- React 完成渲染,发现 Counter 组件的
state变了,需要更新DOM。 - 在 commit 阶段,它把按钮里的文本从 0 修改为 1 。
至此,一次
setCount的旅程就完成了。从用户点击到界面更新,每一步都清晰地对应了我们之前讨论的优先级管理和调度机制。
复杂一点的例子,列表更新
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React 源码', completed: false },
{ id: 2, text: '写一个 Demo', completed: false },
{ id: 3, text: '喝杯咖啡', completed: false },
]);
const handleToggle = (id) => {
setTodos(currentTodos =>
currentTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
);
}
function TodoItem({ todo, onToggle }) {
console.log(`渲染 TodoItem: ${todo.text}`);
return (
<li
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
);
}
假设我们点击了第一项 “学习 React 源码”。
第 1-5 步:更新入队与调度 (和之前类似)
- 触发更新 :
handleToggle(1)被调用。 - 创建 Update : setTodos 导致 React 创建一个
update对象。这次的payload是一个函数:currentTodos => currentTodos.map(...)。 - 入队 : 这个 update 对象被放入 TodoList 组件对应 Fiber 的
updateQueue中。 - 调度 :
scheduleUpdateOnFiber被调用,在 Root 上标记pendingLanes。 - 开始渲染 : Scheduler 启动,React 从根节点开始 render 阶段。
第 6 步: beginWork 与 processUpdateQueue (在 TodoList 上)
- 渲染工作进行到
TodoList组件。 - React 对 TodoList 执行
beginWork。 processUpdateQueue被调用,它找到了updateQueue里的那个update对象。- 它执行
payload函数,基于旧的 todos state 计算出 新的 todos state - TodoList 的
memoizedState被更新为这个新数组。
第 7 步:Diff 算法登场 (Reconciliation)
这是最关键的一步! TodoList 的 state 变了,React 需要弄清楚它的子组件( TodoItem 列表)发生了什么变化。它会拿新的 todos 数组生成的虚拟 DOM 和旧的 Fiber 节点进行比较(diffing):
-
比较第一个 TodoItem (id: 1) :
- 旧 Fiber :
<TodoItem key={1} todo={{...completed: false}} ... />- 新 VDOM :
<TodoItem key={1} todo={{...completed: true}} ... />- React 发现
key 相同,组件类型 ( TodoItem ) 也相同。它判断:“ OK,这是同一个组件,不需要销毁重建,只需要更新它。 ” - 但是,它发现 props 变了( todo 对象里的 completed 属性不同)。
- 于是,React 复用 这个 Fiber 节点,并给它打上一个
Update的标记,表示它在稍后的 commit 阶段需要被更新。
-
比较第二个 TodoItem (id: 2) :
- 旧 Fiber :
<TodoItem key={2} todo={{...completed: false}} ... />- 新 VDOM :
<TodoItem key={2} todo={{...completed: false}} ... />key和类型都相同。React 进一步比较 props ,发现 todo 对象 完全没变 。- 优化来了! React 判断:“ 这个
组件和它的子树都没变,我不需要再对它进行beginWork了! ” 这个过程被称为Bailout (保释/退出)。React 会直接复用旧的Fiber 节点,跳过对这个组件的渲染工作。
-
比较第三个 TodoItem (id: 3) :
- 同上, props 也没变,同样触发 Bailout,跳过渲染。 第 8 步:深入被标记的组件 ( beginWork on TodoItem id: 1)
- 因为第一个
TodoItem被标记了Update,所以 React 会继续对它执行beginWork。 - 它接收到新的
props( todo.completed 为 true )。 - 它重新执行
TodoItem函数, style 属性现在是{ textDecoration: 'line-through' }。 - 它对自己的子节点(
li 和 文本)进行 diff,发现只是 li 的 style 属性变了。
第 9 步:Commit 阶段
- 整个
render 阶段结束后,React收集到了所有需要执行的 DOM 操作。 - 在这个例子里,唯一的 DOM 操作就是: 找到 id 为 1 的那个
li元素,并将它的style.textDecoration更新为line-through。 - 其他
li元素完全不会被触碰。 总结:
这个例子清晰地展示了:
-
状态在哪,就在哪更新 :
update被放在持有 todos 状态的TodoList组件上。 -
processUpdateQueue触发 Diff : TodoList 的状态更新,触发了 React 对其子组件列表的 Diff。 -
Diff 的高效性 :
通过 key ,React 能够识别出哪些组件是复用的,哪些是新增或删除的。 -
Bailout 优化 :对于 props 没有变化的组件,React 会直接跳过它们的渲染,这是 React 高性能的关键。
-
最小化 DOM 操作 :最终,只有真正发生变化的 DOM 节点才会被修改。 所以,你只修改了一项,React 也只会去更新那一项对应的真实 DOM,非常高效。 非常好,新增一个 item 是另一个经典的 diff 场景。我们接着用 TodoList 的例子,但这次是添加一个新 todo。
新增的例子
我们有一个应用,包含一个 TodoList 组件和一个独立的 SearchInput 组件。
-
TodoList:显示一个项目列表,点击 "Add Todo" 按钮会向列表中添加一个新项目。 -
SearchInput:一个输入框,用户输入时会立即显示搜索词。输入操作被认为是高优先级的。
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn Fiber' },
{ id: 2, text: 'Learn Lanes' },
]);
const [searchTerm, setSearchTerm] = useState('');
function handleAddTodo() {
setTodos(prev => [...prev, { id: Date.now(), text: 'New Task' }]);
}
return (
<div>
{/* 高优先级输入框 */}
<SearchInput value={searchTerm} onChange={setSearchTerm} />
<hr />
{/* 低优先级列表 */}
<button onClick={handleAddTodo}>Add Todo</button>
<ul>
{todos.map(todo => <TodoItem key={todo.id} text={todo.text} />)}
</ul>
</div>
);
}
function SearchInput({ value, onChange }) {
return (
<input
value={value}
onChange={e => onChange(e.target.value)}
placeholder="Type here (high priority)"
/>
);
}
function TodoItem({ text }) {
// 为了模拟耗时操作,我们在这里空转一下
const now = performance.now();
while (performance.now() - now < 3) {
// Do nothing for 3ms to simulate work
}
return <li>{text}</li>;
}
第 1 步:用户点击 "Add Todo" (低优先级更新)
- 触发更新 :
handleAddTodo被调用,执行setTodos。 - 创建与入队 :React 创建一个
update对象
{
lane:DefaultLane,
payload:prev => [...prev, ...],
next:null
}
- 这个 update 被放入
TodoApp组件对应 Fiber 的updateQueue中。这是一个 O(1) 操作。 - 调度更新 :
scheduleUpdateOnFiber被调用。React 为这次更新分配一个 低优先级的 Lane (例如 DefaultLane ),并将其添加到FiberRoot的pendingLanes中。 - 请求工作循环 :React 向调度器 (
Scheduler) 请求一个新的工作循环 (workLoop) 来处理这个待办任务。
第 2 步: workLoop 开始 - 构建 workInProgress 树
workLoop 的核心是 performUnitOfWork ,它一次处理一个 Fiber 节点,然后移动到下一个,形成一个深度优先遍历。
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
-
performUnitOfWorkonTodoApp:-
beginWork在 TodoApp Fiber 上执行。 -
processUpdateQueue:React 遍历 TodoApp 的updateQueue,执行 payload 函数,计算出新的 todos state。 -
React 调用
render,得到新的 Virtual DOM 结构。然后,它将这个新 VDOM与current Fiber树(上次渲染的结果)进行比较。SearchInput:Props 没有变化,React 复用 current Fiber,并将其作为 TodoApp 的 child 。- button :同上,复用。
- ul :同上,复用。
- ul 的子节点 ( TodoItems) :
TodoItem 1 & 2:React 发现 key 和 type 都匹配。它会复用 current Fiber来创建workInProgress Fiber。 props ( text ) 也没变。它将 TodoItem 1 设置为 ul 的 child ,然后将 TodoItem 2 设置为 TodoItem 1 的 sibling 。- TodoItem 3 (New) :React 发现这是一个新的
key。它会创建一个新的 FiberNode,并给它打上Placement标记(表示这是一个需要插入 DOM 的新节点)。这个新 Fiber 被设置为 TodoItem 2 的 sibling 。
-
pendingProps :在为
TodoItem 3创建新 Fiber 时,从 VDOM 传入的 props ( { text: 'New Task' } ) 被记录在该 Fiber 的pendingProps属性上。
-
2. performUnitOfWork on TodoItem 1 & 2 :
- beginWork 在
TodoItem 1上执行。它发现 props 没有变化,这是一个“Bailout”优化。 触发bailout需要满足的核心条件
| 条件类别 | 具体条件 | 简要说明 |
|---|---|---|
| 核心四条件 | 1. Props 全等 | oldProps === newProps(注意是全等比较,非浅比较)。 |
| 2. Context 未变化 | 所使用的Context值没有发生变化。 | |
| 3. Fiber类型未变 | 组件类型未改变(如div未变为p)。 | |
| 4. 自身无相关更新 | 当前组件上不存在与本次渲染优先级匹配的待处理状态更新。 |
- React 会跳过这个组件的 render ,直接复用上次的子节点,然后
移动到它的sibling,即 TodoItem 2。 - TodoItem 2 同理。
第 3 步:中断!用户在 SearchInput 中输入
假设 workLoop 刚刚处理完 TodoItem 2,正准备开始处理 TodoItem 3。此时,用户在输入框里按了一个键。
- 高优先级更新 : SearchInput 的
onChange被触发,调用setSearchTerm。 - 调度 :React 创建一个
update,并为其分配一个 高优先级的 Lane (例如InputContinuousLane)。这个 Lane 被添加到FiberRoot的pendingLanes中。 shouldYield() 返回 true:workLoopConcurrent在处理每个 Fiber 后都会调用 shouldYield() 。这个函数会检查是否有更高优先级的任务(通过比较 lanes )或者是否渲染时间过长。现在,它发现了一个更高优先级的 InputContinuousLane ,于是返回 true 。- 暂停工作
第 4 步:执行高优先级任务
- React 立即开始一个
新的 workLoop,但这次的 renderLanes 只包含高优先级的InputContinuousLane。 - 它从 root 开始,快速地只处理与 InputContinuousLane 相关的更新。
- beginWork on TodoApp :
processUpdateQueue运行时,它会跳过低优先级的 setTodos 更新,只处理高优先级的setSearchTerm更新。 beginWork on SearchInput: props 变化了 ( value ),它会重新渲染 SearchInput 。- 这个过程非常快,因为它
跳过了所有 TodoItem的处理。 - 高优先级任务的
workInProgress 树很快构建完成,并被提交(Commit),用户在输入框中立即看到了反馈。
第 5 步:恢复低优先级任务
- 高优先级任务完成后,React 发现
FiberRoot的pendingLanes中还有之前那个低优先级的DefaultLane。 - 它启动一个新的
workLoop来处理DefaultLane。 - React 从头开始创建workInProgress 树。
- beginWork on TodoItem 3:这是一个新 Fiber ( Placement )。它会执行 TodoItem 函数组件,创建 li VDOM。它的
pendingProps ( { text: 'New Task' } )被用来渲染。 - TodoItem 3 没有 child ,所以 beginWork 完成。
beginWork 的核心任务就是 比较 (diffing)。它将 render 函数返回的新 Virtual DOM 元素与 current Fiber 树上的旧子节点进行比较,然后产生带有“副作用标记”(Side-effect Tags)的 workInProgress 子节点。
这些标记就是告诉“Commit 阶段”需要执行什么 DOM 操作的指令:
Placement: 这是一个新节点,需要在 DOM 中 插入 它。Update: 这是一个现有节点,但它的 props 或 state 变了,需要在 DOM 中 更新 它的属性。Deletion: 这个节点在新 VDOM 中消失了,需要在 DOM 中 删除 它。 这些带标记的 Fiber 节点会在 completeWork 阶段被收集到一个叫做effectList的链表中,最终由 Commit 阶段统一执行。
第 6 步: completeWork 和 Commit
- completeWork 阶段 :
当一个节点(和它的所有子孙节点)的 beginWork 都完成后, completeWork 会被执行。这个过程从下往上“冒泡”。
- completeWork on TodoItem 3:对于带
Placement标记的Fiber,它会创建真实的 DOM 节点 ( <li> ),并将pendingProps设置到DOM 节点上。 - 然后它将这个新
Fiber(现在带着真实的 DOM 节点)的“副作用”( Placement )冒泡到父节点 ul 的effectList上。 - completeWork 冒泡经过 TodoItem 2, 1, ul , button ... 直到 TodoApp 。
- Commit 阶段 :当整个
workInProgress树都 complete 后,React 进入 Commit 阶段。
- 它遍历
effectList,发现 TodoItem 3 的 Fiber 有一个Placement副作用。 - 它执行 DOM 操作:将新创建的 li 元素插入到 ul 的末尾。
pendingProps->memoizedProps:在 Commit 阶段的最后,FiberRoot 的 current指针会切换到刚刚完成的workInProgress树。此时,树上所有 Fiber 的 memoizedProps 都会被更新为pendingProps 的值。这标志着本次更新成功完成,pendingProps正式“转正”,成为下一次渲染的“旧 props”。 至此,一个包含中断和恢复的完整更新流程就完成了。React 通过Fiber 链表、 workInProgress 树和Lanes 模型,优雅地实现了可中断渲染和优先级调度,确保了在高负载下也能提供流畅的用户体验。
-
Render 阶段(可中断) :在这个阶段,React 在 requestIdleCallback 里只构建 Fiber 树和计算 state, 不触碰真实 DOM 。
-
Commit 阶段(不可中断) :在计算完成后,React 会一次性、同步地将所有变更应用到 DOM 上。
workLoop, performUnitOfWork, 和 shouldYield 的关系
workLoop是一个 while 循环,是渲染工作的总引擎。performUnitOfWork是循环体里执行的核心函数,它的职责就是 处理好一个 Fiber 节点 。这个“处理好”包括:- 调用
beginWork(diff 子节点,计算新状态等)。 - 如果
beginWork产生了子节点,那么下一个要处理的就是这个child Fiber。 - 如果没有子节点,就调用
completeWork,然后寻找sibling或return节点作为下一个处理对象。
- 调用
shouldYield()是用来决定 workLoop 是否应该暂停的“刹车”。在并发模式下 ( workLoopConcurrent ),每当 performUnitOfWork 完成一个工作单元 后, while 循环的条件就会检查 shouldYield() 。
伪代码如下:
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function workLoopConcurrent() {
// 只要还有工作要做,并且调度器没让停,就一直干
while (workInProgress !== null && !shouldYield()) {
// 干一个活儿 (处理一个 Fiber)
performUnitOfWork(workInProgress);
}
}
所以,这个过程是“ 干一个活儿,看一眼表,干一个活儿,看一眼表... ”的模式,而不是“把所有活儿干完再看表”。这保证了 React 可以非常及时地响应更高优先级的任务。
两种工作循环的源码与行为对比
这种差异直接体现在 workLoop的源码实现上:
- 同步工作循环 (
workLoopSync) :在同步模式下,循环会一次性处理完所有工作单元,不可中断。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress); // 持续工作,直到没有下一个节点
}
}
- 并发工作循环 (
workLoopConcurrent) :在并发模式下,循环在每次处理工作单元前都会检查是否需要让出主线程。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这里的 shouldYield()函数是并发调度的“指挥官”,它主要根据两点做出决定:
- 时间片耗尽:默认一个时间片约为 5毫秒。如果工作超过5ms,
shouldYield()返回true,中断当前循环,避免阻塞浏览器渲染和用户交互。 - 更高优先级任务出现:即使时间片未用完,如果有更高优先级任务(如用户输入)准备执行,
shouldYield()也会返回true,让高优先级任务“插队”。
🔄 高优先级任务如何中断与“插队”
在并发模式下,高中低不同优先级的任务,其命运也各不相同。这一切都依赖于 Lane 模型的精妙设计。React 使用一个31位的二进制数来表示不同的“车道”(Lane),每条车道代表一种优先级,位数越高优先级越低。
- 中断低优先级任务:当一个高优先级任务(如
SyncLane代表的用户点击事件)产生时,调度中心会通过cancelCallback取消当前正在执行的低优先级任务(将其回调函数设置为null),从而中断正在进行的workLoopConcurrent。 - 执行高优先级任务:中断后,React 会安排并执行高优先级任务对应的
workLoop。 - 重启低优先级任务:待高优先级任务执行完毕后,React 会检查是否还有未被处理的低优先级任务(即“车道”上是否还有任务),然后重新调用
ensureRootIsScheduled来调度执行之前被中断的任务。这也就是流程图中所展示的循环。
⚠️ 防范“饥饿问题”
你可能会担心,如果一直有高优先级任务,低优先级任务是否会被无限期推迟(即“饥饿问题”)。React 设计了过期时间(Timeout)机制来应对。每个低优先级任务(如 Transition更新)都有一个预设的超时时间(例如5秒)。如果任务由于一直被中断而超过这个时间还未完成,它的优先级会被提升到最高(同步级别),从而能够被立即执行,这就避免了“饥饿问题”的发生。
💡 对开发者的意义
理解这些机制有助于我们写出更高效的React代码:
- 使用
startTransition和useDeferredValue:对于非紧急更新(如搜索筛选、加载大量数据),使用这些并发API将它们标记为低优先级,避免阻塞用户交互。 - 优化性能:意识到渲染是可中断的,减少不必要的组件重新渲染(使用
React.memo,useMemo,useCallback)仍然非常重要,因为这能缩短单个时间片内的计算量,让交互响应更快。