前言

随着 AI 技术深入渗透前端开发领域,React 因其组件化设计与声明式编程理念,在与 AI 协作方面表现良好,进一步放大了其优势,已成为现代前端项目选型的热门方案。

然而,React(尤其是近几个版本)引入了大量新概念和特性,学习曲线陡峭。如果使用时只知其然而不知其所以然,很容易陷入误用、滥用的陷阱,为项目埋下难以察觉的 BUG。

对于追求卓越的前端开发者而言,深入理解主流框架的底层实现原理,不仅有助于在日常开发中写出更高质量的代码、更好地解决复杂问题,还能在与 AI 协作时更准确地评估其方案和代码质量,避免代码库被”带入坑”而逐渐腐化。

本文将以 React 19 为基准,系统探讨 Virtual DOM、Diff 算法、Fiber 架构、Hooks 等核心特性的实现原理。为便于理解,文中会适当引用源码片段并做必要的简化调整。


一、Virtual DOM

Virtual DOM(虚拟 DOM)是对真实 DOM 的轻量级 JavaScript 对象抽象。在 React 中,Virtual DOM 通常以 ReactElement 的形式存在。

React 会在内存中维护一套用于调度更新的 Fiber 树(基于 ReactElement 构建),通过比较新旧 Fiber 树的差异(即 Diff 算法),计算出最小的 DOM 变更集合,最终高效地更新真实 DOM。

// 真实 DOM
<div class="container">
<h1>Hello</h1>
<p>World</p>
</div>
// Virtual DOM(简化版 React Element,实际还包含 key、ref 等属性)
{
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } },
{ type: 'p', props: { children: 'World' } }
]
},
// key: 'unique-key', // 用于列表 Diff 的关键属性
// ref: null, // 用于直接访问真实 DOM 的引用
}

补充:React 19 配套的 React Compiler 可自动优化 ReactElement 的生成,减少不必要的对象创建和后续 Diff 开销。

那么,引入 Virtual DOM 这层抽象,究竟能带来哪些价值呢?

Virtual DOM 的好处

易于实现跨平台渲染

Virtual DOM 本质上是平台无关的 JavaScript 对象。要实现一个支持特定平台的渲染器,只需要实现一组核心接口即可(以下为简化版核心接口,实际包含更多生命周期钩子):

// 渲染器需要实现的核心接口(简化版)
const HostConfig = {
createInstance, // 创建元素实例(如 DOM 元素、Native View)
createTextInstance, // 创建文本节点实例
appendChild, // 添加子节点
insertBefore, // 插入节点
removeChild, // 删除节点
commitUpdate, // 提交属性更新
setTextContent, // 更新文本内容
// ... 还有 prepareForCommit, resetAfterCommit 等辅助钩子
}

不同平台只需实现这些接口,即可将 Virtual DOM 转换为各自平台的渲染目标:

// 同一个Virtual DOM(平台无关)
const vnode = {
type: 'button',
props: {
className: 'btn',
children: 'Click Me'
}
};
// ReactDOM (浏览器) 的实现
createInstance(type, props) {
const element = document.createElement(type);
element.className = props.className;
return element;
}
// React Native (移动端) 的实现 (概念示意)
createInstance(type, props) {
// 内部通过 Fabric 渲染器映射到 iOS/Android 的原生控件
return NativeViewHierarchyManager.createView(type, props);
}
// ReactDOMServer (服务端渲染) 的实现
// 服务端不维护“实例”,而是直接生成 HTML 字符串
function renderToString(vnode) {
if (typeof vnode === 'string') return vnode;
const { type, props } = vnode;
const children = React.Children.toArray(props.children)
.map(renderToString)
.join('');
return `<${type} class="${props.className}">${children}</${type}>`;
}

通过这种设计,开发者只需编写 Virtual DOM(通常通过 JSX 语法),无需关心底层渲染的是浏览器 DOM、移动端 Native View,还是服务端的 HTML 字符串。

保证性能下限

诚然,Virtual DOM 引入了额外的 JavaScript 计算开销,但它能有效避免手动操作 DOM 时的常见性能陷阱,为应用提供稳定的性能保障:

  • 减少 DOM 操作频率:直接操作 DOM 时,若频繁读写属性(尤其是交替读取布局属性、写入样式),可能触发多次浏览器回流 / 重绘。Virtual DOM 在 JavaScript 层完成 Diff 计算后,会一次性将差异应用到真实 DOM,最小化 DOM 操作次数。

  • 配合调度机制优化更新:React 18+ 引入的自动批处理和并发调度,会将多个状态更新合并后再触发Virtual DOM 的 Diff,进一步减少不必要的渲染(如下例中连续 setCount 最终仅渲染一次)。

// 直接操作 DOM - 交替读写触发多次回流(典型性能陷阱)
const el = document.getElementById('box')
const height = el.offsetHeight // 读取布局属性,触发回流
el.style.height = height + 10 + 'px' // 写入样式,触发回流
const width = el.offsetWidth // 再次读取,又触发回流
// React 批量更新 - 合并为一次 DOM 操作
const [count, setCount] = useState(0)
setCount(1)
setCount(2)
setCount(3) // React 18+ 自动批处理,仅触发一次 Diff 和渲染

补充:React 19 配套的 React Compiler 可自动优化组件渲染,减少不必要的Virtual DOM 生成和 Diff,进一步提升性能下限。

简化框架实现与降低心智负担

在实现”数据变化 → 精确更新 DOM”这一目标时,主流框架通常采用两种思路:

  • 响应式依赖追踪(如 Vue):通过 Proxy / Object.defineProperty 监听数据变化,维护”数据 → DOM 节点”的映射关系。数据变化时,框架可直接定位并更新相关节点。这种方式性能出色,但框架内部需要实现复杂的依赖收集和追踪逻辑。

  • Virtual DOM + Diff(如 React):无需维护复杂的依赖关系,只需在数据变化时生成新的 Virtual DOM 树,通过 Diff 算法与旧树对比,计算出差异后再更新真实 DOM。这种方式在框架实现上更为简洁,且天然契合声明式编程理念。

Virtual DOM 与 React 声明式编程理念的配合,让开发者只需关注”UI 应该呈现什么状态”(描述 state → UI 的映射关系),而无需关心”如何手动更新 DOM”。这种设计既简化了框架内部实现,也显著降低了开发者的心智负担。


Virtual DOM 的 Diff 算法

React 16 引入 Fiber 架构后,Virtual DOM 的工作方式发生了重要变化:Virtual DOM 作为 Fiber 的输入,会先被转换为 Fiber 节点,后续的 Diff 算法在 Fiber 链表上执行,而非直接操作 Virtual DOM 树。

相应地,遍历方式也从 React 15 的同步递归遍历 Virtual DOM 树,演进为迭代遍历 Fiber 链表,并支持任务的暂停与恢复——这为后续的并发渲染奠定了基础。

而这,正是我们下一章节要深入探讨的主题——Fiber 架构。


二、Fiber 架构

React 16 引入的 Fiber 架构,究竟要解决什么问题?

要回答这个问题,我们需要先了解 React 15 时代的性能痛点。

Stack Reconciler 的局限性

React 15 及之前版本采用 Stack Reconciler(栈协调器),通过同步递归的方式渲染组件树。

这种方式存在一个致命缺陷:渲染任务一旦启动就无法中断。如果组件树规模较大或单个组件渲染耗时较长,主线程会被长时间占用,导致页面卡顿甚至假死。

// 同步递归,无法中断
function render() {
const children = virtualDOM.children
children.forEach(child => {
renderElement(child) // 递归调用,需连续执行完
})
}

来看一个具体例子:

function App() {
return (
<div>
<Header />
<MainContent>
<List items={10000} />
<Chart data={largeData} />
</MainContent>
<Footer />
</div>
)
}

如果 ListChart 组件渲染耗时较长,整个渲染过程可能会占用主线程几百毫秒。考虑到浏览器的渲染、用户交互事件(点击、输入等)都依赖主线程执行,这期间用户的任何操作都将无法得到响应。

显然,随着应用复杂度提升,React 15 的同步渲染模式已成为性能瓶颈。React 团队意识到,必须设计一种更灵活的架构来解决这个问题。

在这一背景下 Fiber 架构应运而生。

Fiber 架构如何解决问题

既然核心问题在于“渲染任务过大且不可中断”,那么解决思路自然就是任务拆解:将庞大的渲染任务拆分为多个小型工作单元,使这些单元可以暂停、恢复,从而优先响应用户交互等高优先级任务。

React 团队的方案可概括为两点:

  1. 以 Fiber 节点作为最小工作单元
  2. 将渲染过程拆分为 Render 阶段和 Commit 阶段

Render 阶段(可中断)

这个阶段的主要工作是计算和标记,不涉及真实 DOM 的操作,因此可以安全中断:

  • 构建或更新 Fiber 树(基于 ReactElement 生成)
  • 收集需要执行的副作用(如 DOM 更新、生命周期调用等)
  • 使用 flags 标记具体操作类型(插入、更新、删除等)

为实现可中断调度,Fiber 架构借鉴了操作系统的任务调度思想,引入了以下机制:

  • 时间片调度:每个工作单元的执行时间控制在约 5ms 内(基于浏览器帧率优化),执行完成后主动让出主线程
  • 优先级调度:高优先级任务(如用户点击)可打断低优先级任务(如列表渲染),避免重要任务被“饥饿”
  • 并发渲染:React 18 引入的特性,支持不同优先级任务的并行渲染(React 19 在此基础上进一步优化了 Suspense、Actions 等并发场景的体验)

Commit 阶段(不可中断)

这个阶段负责将 Render 阶段的计算结果应用到真实 DOM。为保证 DOM 状态的一致性,此阶段必须同步执行且不可中断:

  • 按照 flags 标记执行真实 DOM 节点操作
  • 执行生命周期和 Hook 回调(如 useLayoutEffectcomponentDidMount 等)
  • 更新 ref 引用
  • 调度 useEffect 被动副作用(在浏览器空闲时执行)

通过这种“可中断的 Render 阶段 + 不可中断的 Commit 阶段”设计,React 既保证了渲染的灵活性,又有效避免了主线程长时间被占用的问题。

那么,上述特性是如何通过 Fiber 架构来实现的呢?让我们深入了解一下。

Fiber 节点结构

在源码的 react-reconciler 包中,ReactFiber.js 文件定义了 Fiber 节点的数据结构。

Fiber 节点基于 ReactElement 构建,复用了其 typekey 等属性。多个 Fiber 节点通过 child(首个子节点)、sibling(下一个兄弟节点)、return(父节点)三个指针串联成树状结构。

以下是 Fiber 节点的核心数据结构及属性说明:

interface Fiber {
// ==================== 节点类型信息 ====================
tag: WorkTag // 节点类型标记(如函数组件、类组件、原生 DOM 节点等)
type: any // 关联的组件函数/类、或原生 DOM 标签名(与 ReactElement.type 一致)
key: string | null // 复用 ReactElement 的 key,用于列表 Diff
elementType: any // 通常与 type 一致,在某些场景(如懒加载组件)下会不同
stateNode: any // 对应的真实 DOM 节点、类组件实例,或 null(函数组件无实例)
mode: number // 渲染模式标记(如并发模式、严格模式等)
// ==================== Fiber 树结构指针 ====================
return: Fiber | null // 指向父 Fiber 节点
child: Fiber | null // 指向第一个子 Fiber 节点
sibling: Fiber | null // 指向下一个兄弟 Fiber 节点
index: number // 当前节点在父节点 children 中的索引(用于列表 Diff)
// ==================== 状态与 Props ====================
pendingProps: any // 新传入的 props(待处理)
memoizedProps: any // 上次渲染完成后的 props
memoizedState: any // 上次渲染完成后的 state(函数组件中存储 hooks 链表)
updateQueue: UpdateQueue<any> | null // 更新队列(存储 setState、useState 等产生的更新)
dependencies: Dependencies | null // 存储 context 依赖、hooks 依赖等
// ==================== 副作用标记 ====================
flags: Flags // 当前节点的副作用标记(如插入、更新、删除等)
subtreeFlags: Flags // 子树的副作用标记(快速判断子树是否有需要处理的副作用)
deletions: Array<Fiber> | null // 需要删除的子 Fiber 节点列表
// ==================== 调度优先级 ====================
lane: Lane // 当前节点的更新优先级
childLanes: Lane // 子树的更新优先级(快速判断子树是否有高优先级更新)
// ==================== 双缓存树 ====================
alternate: Fiber | null // 指向另一棵树的对应节点(连接 current 树和 workInProgress 树)
}

补充:双缓存树是 Fiber 架构的核心设计之一。React 在内存中同时维护两棵树——current 树(当前屏幕显示的内容)和 workInProgress 树(正在构建的新树)。两棵树通过 alternate 指针相互连接,当 workInProgress 树构建完成后,React 只需交换根节点指针即可完成更新。

Fiber 树的处理

从前述数据结构可以看出,Fiber 通过 childsiblingreturn 三个指针将多个节点串联成有序的树结构,从而实现了按特定顺序处理节点的能力。

那么,Fiber 具体是如何逐个处理节点的呢?让我们通过源码来一探端倪。

packages/react-reconciler/src/ReactFiberWorkLoop.js
// 并发调度工作循环
// 注:源码中还有 workLoopConcurrent、workLoopSync 等变体
function workLoopConcurrentByScheduler() {
// 持续执行直到 Scheduler 要求让出主线程
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
// 执行单个 Fiber 节点的工作单元
function performUnitOfWork(unitOfWork: Fiber) {
const current = unitOfWork.alternate
let next = beginWork(current, unitOfWork, entangledRenderLanes)
if (next === null) {
// 没有子节点,完成当前节点
completeUnitOfWork(unitOfWork)
} else {
// 有子节点,继续处理子节点
workInProgress = next
}
}
// 完成单个 Fiber 节点的处理,并寻找下一个待处理节点
function completeUnitOfWork(unitOfWork: Fiber) {
let completedWork = unitOfWork
do {
const current = completedWork.alternate
// 调用 completeWork 完成当前节点的收尾工作
// 返回下一个要处理的 Fiber(第一个子节点)
const next = completeWork(current, completedWork, renderLanes)
if (next !== null) {
workInProgress = next
return
}
// 如果没有子节点,检查是否有兄弟节点
if (completedWork.sibling !== null) {
workInProgress = completedWork.sibling
return
}
// 没有兄弟节点,回到父节点
completedWork = completedWork.return
workInProgress = completedWork
} while (completedWork !== null)
// 回到根节点,整个 Render 阶段完成
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted
}
}

以上是工作循环,以及 Fiber 节点的执行入口、完成入口函数。

performUnitOfWork 中,我们可以看到,通过调用 beginWork 开始一个节点的处理,处理完后会返回下一个 Fiber(首个子节点),我们先了解下 beginWork 都做了些什么:

packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork(
current: Fiber | null, // 上一次渲染时的 Fiber 节点(如果存在),用于进行对比
workInProgress: Fiber, // 当前正在处理的 Fiber 节点(本次渲染的副本)
renderLanes: Lanes, // 当前渲染的优先级车道(Lanes 模型)
): Fiber | null /* 返回下一个要处理的 Fiber 节点,如果没有则返回 null */ {
// current 不为 null 表示这是一个更新(而非首次挂载)
if (current !== null) {
// 删减...
// 此分支中,主要做两件事:
// 1. 如果 props/context 未变化,且没有待处理的更新,则调用 `attemptEarlyBailoutIfNoScheduledUpdate`,
// 复用旧节点,直接跳到【返回子节点】
// 2. 检测是否需要各种条件,确定是否需要标记该 Fiber 需要执行更新,即 `didReceiveUpdate` 的值是否设置为 `true`
// 删减...
}
}
// current 为 null 表示这是首次渲染(mount)
else {
// 首次渲染,自然没有接收到更新
didReceiveUpdate = false;
}
// 在进入 begin 阶段之前,清除待处理的更新优先级
workInProgress.lanes = NoLanes;
// 根据 Fiber 节点的 tag(类型)执行不同的更新逻辑(每个 case 对应一种 React 组件类型或节点类型)
switch (workInProgress.tag) {
// 以函数式组件为例子
case FunctionComponent: {
const Component = workInProgress.type;
// 调用 `updateFunctionComponent(...)` 内部执行:
// │
// ├── nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes)
// │
// ├── reconcileChildren(current, workInProgress, nextChildren, renderLanes) // Diff 算法的入口
// │ ├── 是首次渲染 -> workInProgress.child = mountChildFibers() // 直接创建子 Fiber
// │ └── 是更新渲染 -> workInProgress.child = reconcileChildFibers() // 核心 Diff 逻辑
// │ ├── 对比旧的子 Fiber 链表和新的 ReactElement 树
// │ ├── 生成新的子 Fiber 链表
// │ └── 标记需要插入、删除、移动的副作用 (Flags)
// │
// └── return workInProgress.child // 返回子 Fiber 或 null
return updateFunctionComponent(
current,
workInProgress,
Component, // 函数组件本身
workInProgress.pendingProps, // 传入的 props
renderLanes,
);
}
// 略过以下分支(参考函数式组件):
// LazyComponent、ClassComponent、HostRoot、HostHoistable、HostSingleton、HostComponent、HostText、SuspenseComponent、
// HostPortal、ForwardRef、Fragment、Mode、Profiler、ContextProvider、ContextConsumer、MemoComponent、SimpleMemoComponent、
// IncompleteClassComponent、IncompleteFunctionComponent、SuspenseListComponent、ScopeComponent、ActivityComponent、
// OffscreenComponent、LegacyHiddenComponent、CacheComponent、TracingMarkerComponent、ViewTransitionComponent、Throw
}
throw new Error(/*删减*/)
}

不符合

符合条件

beginWork

是否首次渲染

更新didReceiveUpdate

清理 Lanes(workInProgress.lanes = NoLanes)

处理函数式组件(Fiber.tag === FunctionComponent)

updateFunctionComponent

renderWithHooks

更新didReceiveUpdate

bailout 优化检查

attemptEarlyBailoutIfNoScheduledUpdate

reconcileChildren

首次渲染?

mountChildFibers

返回子 Fiber/null

reconcileChildFibers

可以发现,beginWork 处理节点,并返回第一个子 Fiber。

beginWork 返回 null 时,则已经到了叶子节点了,此时就需要通过 completeUnitOfWork 来完成一个 Fiber 的处理,并继续寻找下一个 Fiber。

completeUnitOfWork 内部会调用 completeWork 完成收尾工作,我们看看其源码实现:

packages/react-reconciler/src/ReactFiberCompleteWork.js
// 完成 Fiber 节点处理
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 根据 Fiber 类型执行对应的完成逻辑
switch (workInProgress.tag) {
case FunctionComponent:
case Fragment:
// 函数组件和 Fragment 主要执行属性冒泡
bubbleProperties(workInProgress)
return null
case ClassComponent:
// 类组件:弹出 legacy context,然后冒泡属性
popLegacyContext(workInProgress)
bubbleProperties(workInProgress)
return null
case HostComponent: {
// 原生 DOM 节点
// 1. 创建或更新 DOM 实例
const instance = createInstance(workInProgress.type, newProps)
workInProgress.stateNode = instance
// 2. 处理 DOM 属性
finalizeInitialChildren(instance, workInProgress.type, newProps)
// 3. 冒泡子节点的副作用
bubbleProperties(workInProgress)
return null
}
case HostText: {
// 文本节点
const newText = newProps
workInProgress.stateNode = createTextInstance(newText)
return null
}
// 其他 case...
}
}

可以注意到,completeWork 主要是做一些属性冒泡、节点创建之类的的工作。完成这些收尾工作后,回到 completeUnitOfWork 继续通过 childsiblingreturn 的顺序,查找下一个 Fiber 进行处理。

总结以上代码

React 在工作循环中,以可中断和恢复的方式,采用深度优先遍历算法处理 Fiber 树。概括而言,每个 Fiber 节点的处理分为两个阶段:

  1. 自顶向下
  • workLoopConcurrentByScheduler -> performUnitOfWork -> beginWork
  • 从父节点向子节点推进,负责创建或更新 Fiber、执行 Diff 算法、标记副作用。
  1. 自底向上
  • completeUnitOfWork -> completeWork
  • 从子节点向父节点回溯,负责完成节点收尾(如创建 DOM 实例)、收集并冒泡副作用。

这样的描述可能仍有些抽象。让我们通过一个具体示例来理解,假设有如下组件树:

function App() {
return (
<div className="app">
<Header>
<Logo />
<Nav />
</Header>
<Main>
<Content />
</Main>
</div>
)
}

Fiber 树的遍历顺序如下(数字表示处理顺序):

①App
/ \
②Header ⑦Main
/ \ |
③Logo ④Nav ⑧Content
|
⑤[完成]
|
⑥[兄弟节点已处理]

以初次渲染为例,完整的处理流程如下

1. `App` `beginWork`
- `App` `FunctionComponent`执行组件函数得到 `<div className="app">...</div>`
- 协调子节点生成 `div.app` Fiber 节点作为 `App` Fiber 的第一个子节点
- 返回 `div.app` Fiber
2. `div.app` `beginWork`
- `div.app` `HostComponent`提取子节点`Header``Main`
- 协调子节点生成 `Header` Fiber 节点作为 `div.app` 的第一个子节点
- 返回 `Header` Fiber
3. `Header` `beginWork`
- `Header` `FunctionComponent`执行组件函数得到 `<Logo /><Nav />`
- 协调子节点生成 `Logo` Fiber 节点作为 `Header` 的第一个子节点
- 返回 `Logo` Fiber
4. `Logo` `beginWork`
- `Logo` `FunctionComponent`执行组件函数得到 `<img class="logo" />`
- 协调子节点生成 `img.logo` Fiber 节点作为 `Logo` 的第一个子节点
- 返回 `img.logo` Fiber
5. `img.logo` `beginWork`
- `img.logo` `HostComponent`
- 无自定义子节点
- 返回 `null`到达叶子了
6. `img.logo` 进入 `completeWork`这是第一个完成的节点
- 创建真实DOM: `<img class="logo">`
- DOM 挂载到 `img.logo.stateNode`
- 标记 flags: `Placement`需要插入 DOM
- 冒泡副作用`img.logo` `Placement` 标记合并到 `Logo.subtreeFlags`
- 检查: `img.logo` `sibling`没有下一个节点通过 `return` 指针回到 `Logo`
7. `Logo` 进入 `completeWork`这是第二个完成的节点
- `Logo` 是函数组件 DOM 可创建
- `Logo.stateNode = null`
- 冒泡副作用`Logo.subtreeFlags`包含 `img.logo` `Placement`合并到 `Header.subtreeFlags`
- 检查`Logo` `sibling`存在下一个节点 `Nav`
8. `Nav` `beginWork`
- `Nav` `FunctionComponent`执行组件函数得到 `<nav>...</nav>`
- 协调子节点生成 `nav` Fiber 节点作为 `Nav` 的第一个子节点
- 返回 `nav` Fiber
9. `nav` `beginWork`
- `nav` `HostComponent`
- 无自定义子节点
- 返回 `null`到达叶子了
10. `nav` 进入 `completeWork`这是第 3 个完成的节点
- 创建真实DOM: `<nav>...</nav>`
- DOM 挂载到 `nav.stateNode`
- 标记 flags: `Placement`
- 冒泡副作用`nav` `Placement` 标记合并到 `Nav.subtreeFlags`
- 检查: `nav` `sibling`没有下一个节点通过 `return` 指针回到 `Nav`
11. `Nav` 进入 `completeWork`这是第 4 个完成的节点
- `Nav` 是函数组件 DOM 可创建
- `Nav.stateNode = null`
- 冒泡副作用`Nav.subtreeFlags`包含 `nav` `Placement`合并到 `Header.subtreeFlags`
- 检查`Nav` `sibling`不存在下一个节点通过 `return` 指针回到 `Header`
12. `Header` 进入 `completeWork`这是第 5 个完成的节点
- `Header` 是函数组件 DOM 可创建
- `Header.stateNode = null`
- 冒泡副作用`Header.subtreeFlags`包含 `Logo`+`Nav` `Placement`合并到 `div.app.subtreeFlags`
- 检查: `Header` `sibling`存在下一个节点 `Main`
13. `Main` `beginWork`
- `Main` `FunctionComponent`执行组件函数得到 `<Content />`
- 协调子节点生成 `Content` Fiber 节点作为 `Main` 的第一个子节点
- 返回 `Content` Fiber
14. `Content` `beginWork`
- `Content` `FunctionComponent`执行组件函数得到 `<div class="content">...</div>`
- 协调子节点生成 `div.content` Fiber 节点作为 `Content` 的第一个子节点
- 返回 `div.content` Fiber
15. `div.content` `beginWork`
- `div.content` `HostComponent`
- 无自定义子节点
- 返回 `null`到达叶子了
16. `div.content` 进入 `completeWork`这是第 6 个完成的节点
- 创建真实DOM: `<div class="content">...</div>`
- DOM 挂载到 `div.content.stateNode`
- 标记 flags: `Placement`
- 冒泡副作用`div.content` `Placement` 标记合并到 `Content.subtreeFlags`
- 检查: `div.content` `sibling`没有下一个节点通过 `return` 指针回到 `Content`
17. `Content` 进入 `completeWork`这是第 7 个完成的节点
- `Content` 是函数组件 DOM 可创建
- `Content.stateNode = null`
- 冒泡副作用`Content.subtreeFlags` 合并到 `Main.subtreeFlags`
- 检查`Content` `sibling`不存在下一个节点通过 `return` 指针回到 `Main`
18. `Main` 进入 `completeWork`这是第 8 个完成的节点
- `Main` 是函数组件 DOM 可创建
- `Main.stateNode = null`
- 冒泡副作用`Main.subtreeFlags`包含 `Content` `Placement`合并到 `div.app.subtreeFlags`
- 检查`Main` `sibling`不存在下一个节点通过 `return` 指针回到 `div.app`
19. `div.app` 进入 `completeWork`这是第 9 个完成的节点
- 创建真实DOM: `<div class="app">`
- DOM 挂载到 `div.app.stateNode`
- 组装 DOM: `Header` 的子 DOM`img.logo``nav`)、`Main` 的子 DOM`div.content`插入到 `div.app.stateNode`
- 冒泡副作用`div.app.subtreeFlags`包含所有子节点的 `Placement`标记合并到 `App.subtreeFlags`
- 检查: `div.app` `sibling`没有下一个节点通过 `return` 指针回到 `App`
20. `App` 进入 `completeWork`这是第 10 个完成的节点
- `App` 是函数组件 DOM 可创建
- `App.stateNode = null`
- 冒泡副作用`App.subtreeFlags` 合并到 `RootFiber.subtreeFlags`React 真正的根节点
- 检查`App` `sibling`不存在下一个节点通过 `return` 指针回到 `RootFiber`
21. `RootFiber ` 处理完成Render 阶段结束进入 Commit 阶段此时才会真正执行 DOM 插入更新等操作)。

结合源码分析和上述详细的处理流程,我们可以总结出以下几点核心要点:

1. Render 阶段的纯计算特性

Render 阶段是纯内存计算阶段,全程不操作真实 DOM、不执行用户侧副作用,所有变更仅通过 flags 进行标记,这是其能够安全中断、恢复的核心前提。

2. beginWork 与 completeWork 的职责划分

整个遍历过程通过自顶向下自底向上两个阶段形成闭环,职责清晰解耦:

  • 自顶向下阶段(beginWork):Fiber 树的构建核心。职责包括执行组件渲染、触发子节点协调(Diff 算法入口)、生成子 Fiber 节点、标记当前节点的更新副作用,最终返回下一个待处理的子节点,驱动遍历向下深入。
  • 自底向上阶段(completeWork):节点收尾与副作用聚合。职责包括为原生 DOM 节点创建实例、准备属性更新、通过 bubbleProperties 将子树的所有副作用冒泡到父节点,最终找到下一个待处理的兄弟节点,驱动遍历横向扩展或向上回溯。

3. 副作用收集与冒泡机制

这是 Fiber 架构减少 Commit 阶段无效开销的核心设计:

  • 每个节点的插入、更新、删除等操作,都会通过 flags 在当前节点精准标记,无需提前维护全局更新列表
  • 需要删除的节点会统一收集到父节点的 deletions 数组中,供后续批量处理
  • subtreeFlags 机制会在 completeWork 阶段将所有子节点的 flags 向上冒泡合并,最终根节点的 subtreeFlags 会聚合整棵树的所有变更
  • Commit 阶段无需遍历整棵树,只需通过 subtreeFlags 就能快速判断子树是否有需要处理的副作用,大幅减少无效遍历

4. Render 阶段的产出

Render 阶段完成后,会生成一棵完整的 workInProgress Fiber 树,以及聚合了全量更新标记的 flags/subtreeFlags。这为后续的 Commit 阶段提供了完整、可执行的更新蓝图。

Fiber 的指针设计

Fiber 树通过三个针实现深度优先遍历:

  • child 指针:指向第一个子节点(首次渲染时,会在 beginWork 中创建并返回子节点)
  • sibling 指针:指向下一个兄弟节点,用于当前节点处理完成后寻找兄弟节点
  • return 指针:指向父节点,用于所有兄弟节点处理完毕后返回父节点

每个节点处理完成后,会按优先级查找下一个待处理节点:先尝试 child 方向,无子节点则找 sibling,无兄弟节点则返回 return。通过这种方式,可以按深度优先顺序完整遍历整棵树。

那么,为什么要显式使用指针来组织树结构呢?本质上是为了支持任务的中断与恢复。传统树遍历依赖调用栈(递归)或手动维护栈(迭代),一旦开始就无法中断。而 Fiber 通过节点自身的指针固化遍历状态,中断后可以精准地回到中断位置继续遍历。

核心机制workInProgress 全局指针始终指向当前正在处理的 Fiber 节点。中断后只需读取该指针,就能精准恢复到中断前的遍历位置,无需从头开始。每个 Fiber 节点作为独立的最小工作单元,执行耗时可控,配合 Scheduler 的时间片调度,实现了”化整为零”的更新模式,彻底解决了同步递归长时间阻塞主线程的问题。

任务调度与时间切片

前文中我们详细讲解了 Fiber 节点的可中断遍历流程。这里需要明确的是:Fiber 架构是可中断渲染的基础(通过 return/child/sibling 指针固化遍历状态),而 Scheduler 的时间片调度是触发中断的核心机制,两者紧密配合,彻底解决了 React 15 同步递归阻塞主线程的问题。

时间片的核心作用在于:将长时间的 Render 任务拆分为多个约 5ms 的小任务(基于 60fps 标准,为浏览器渲染预留充足时间),每个小任务执行完成后检查是否需要让出主线程(响应用户交互、浏览器绘制等),从而保证页面流畅度。

接下来,让我们通过分析 React 调度层的核心源码,深入探索其工作机制。

任务调度相关源码

packages/scheduler/src/SchedulerPriorities.js
// 经过精简调整
const NoPriority = 0
const ImmediatePriority = 1 // 立即超时,必须同步执行(如受控组件的同步更新)
const UserBlockingPriority = 2 // 用户交互触发,250ms 超时(如点击、输入)
const NormalPriority = 3 // 普通更新,5000ms 超时(如数据请求后的渲染)
const LowPriority = 4 // 低优先级更新,10000ms 超时(如非关键数据的预加载)
const IdlePriority = 5 // 空闲时执行,2^31-1ms 超时(几乎永不执行)
// packages/scheduler/src/forks/Scheduler.js
const frameInterval = 5 // 单个时间片的最大执行时间(ms)
let taskQueue = [] // 任务队列:最小二叉堆,按 sortIndex(即 expirationTime)排序
let currentTask = null // 当前正在执行的任务
let startTime = -1 // 当前时间片的开始时间
let isHostCallbackScheduled = false
let isMessageLoopRunning = false
// 1. 核心入口:添加指定优先级的任务
// 最终会在协调器的 ensureRootIsScheduled 中被调用
//(packages/react-reconciler/src/ReactFiberRootScheduler.js)
// 衔接协调器的 performWorkOnRoot
// 注:实际实现使用双队列(timerQueue 延迟队列 + taskQueue 就绪队列)
// 此处仅使用 taskQueue 说明,不影响理解
function unstable_scheduleCallback(priorityLevel, callback) {
const currentTime = getCurrentTime()
// React 任务调度的优先级通过不同超时时间体现
// 调度器采用”超时优先”策略:任务一旦过期(高优先级任务易过期),
// 则忽略时间片限制,必须同步执行。这是优先级的兜底保障。
var timeout
switch (priorityLevel) {
case ImmediatePriority:
// 立即超时
timeout = -1
break
case UserBlockingPriority:
// 用户交互需尽快响应,设置较短超时
timeout = userBlockingPriorityTimeout // 250
break
case IdlePriority:
// 空闲时执行,超时时间极长
timeout = maxSigned31BitInt // 1073741823
break
case LowPriority:
// 低优先级,超时时间较长
timeout = lowPriorityTimeout
break
case NormalPriority:
default:
// 普通优先级
timeout = normalPriorityTimeout
break
}
// 创建任务对象
const newTask = {
id: taskIdCounter++,
callback, // 协调器注册的更新入口:performWorkOnRoot
priorityLevel, // 优先级
startTime: currentTime, // 开始时间
expirationTime: currentTime + timeout, // 过期时间
sortIndex: currentTime + timeout, // 最小堆排序依据
}
// 插入最小二叉堆(O(log n) 时间复杂度)
// 高优先级任务排在堆顶,被优先执行
push(taskQueue, newTask)
// 若当前无正在调度的任务,启动调度
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true
requestHostCallback()
}
return newTask
}
// 2. 启动调度:优先使用 MessageChannel
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
// schedulePerformWorkUntilDeadline 是 performWorkUntilDeadline 的包裹
// 优先使用 MessageChannel(无 4ms 最小延迟),不支持则降级到 setTimeout/setImmediate(Node.js)
schedulePerformWorkUntilDeadline()
}
}
// 3. 执行任务直到时间片耗尽
function performWorkUntilDeadline() {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime()
// 记录开始时间,用于测量主线程阻塞时长
startTime = currentTime
let hasMoreWork = true
try {
// flushWork 内部调用 workLoop 执行任务
// workLoop 返回是否还有剩余工作
hasMoreWork = flushWork(currentTime)
} finally {
if (hasMoreWork) {
// 仍有工作,继续调度
schedulePerformWorkUntilDeadline()
} else {
isMessageLoopRunning = false
}
}
}
}
// 4. 调度工作循环:取出任务并执行
function workLoop(initialTime: number) {
let currentTime = initialTime
currentTask = peek(taskQueue) // 从最小堆取出最高优先级任务(O(1) 时间复杂度)
while (currentTask !== null) {
// 检查是否应该让出主线程
// 注意:expirationTime 是任务优先级的体现
// 高优先级任务超时时间短,一旦超时就不能让出主线程,必须立即处理
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 当前任务未过期且时间片已用完,暂停执行
break
}
// 执行任务回调
const callback = currentTask.callback
if (typeof callback === 'function') {
currentTask.callback = null
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime
// 执行任务(协调器的 performWorkOnRoot)
const continuationCallback = callback(didUserCallbackTimeout)
currentTime = getCurrentTime()
// 若任务返回回调,说明任务被中断
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback
} else {
// 任务完成,从队列移除
pop(taskQueue)
}
} else {
pop(taskQueue)
}
currentTask = peek(taskQueue)
}
if (currentTask !== null) {
// 仍有未完成任务,时间片已耗尽
return true
} else {
// 全部完成
return false
}
}
// 检查是否应该让出主线程
function shouldYieldToHost() {
// 实验性 API:配合 requestPostAnimationFrame,避免在绘制前执行 JS
if (enableRequestPaint && needsPaint) {
return true
}
const timeElapsed = getCurrentTime() - startTime
if (timeElapsed < frameInterval) {
// 占用主线程时间较短,可继续执行
return false
}
// 时间片已用完,需让出主线程
return true
}

从以上代码可以发现,React Scheduler 的核心职责是 「按优先级调度任务 + 按时间片触发中断」,具体逻辑如下:

混合调度策略

  • 平时状态taskQueue 作为最小二叉堆,按 expirationTime 排序,优先执行过期时间早的任务
  • 兜底机制:若任务已过期(expirationTime <= currentTime),则忽略 shouldYieldToHost(),必须同步执行完,避免任务”饥饿”

时间片中断机制

  • 单个时间片默认最大执行 5ms(基于 60fps 标准,为浏览器渲染、布局、绘制预留 11.6ms)
  • 每执行完一个任务后检查 shouldYieldToHost()

与协调器的衔接

  • Scheduler 的 callback 对应协调器的 performWorkOnRoot(间接调用)
  • performWorkOnRoot 内部流程:
    • 准备工作:获取更新优先级、判断是否为并发模式
    • 执行 Render:调用 workLoopConcurrent(并发模式,可中断)或 workLoopSync(同步模式,不可中断)
    • 收尾处理:Render 完成则进入 Commit 阶段,未完成则返回 continuationCallback 通知 Scheduler 继续调度

Diff 算法

在 Virtual DOM 章节中我们提到,React 15 及之前版本的 Diff 算法采用同步递归遍历虚拟 DOM 树,React 16+ 则基于 Fiber 链表实现可中断的迭代遍历(替代同步递归)。但无论数据结构如何变化,Diff 算法的核心策略始终一致:通过启发式假设将传统树 Diff 的复杂度从 O(n³) 降低到 O(n)。

Diff 算法的三大策略

React 基于以下启发式假设设计了高效的 Diff 策略(从 React 15 延续至今):

  1. 同层比较:只比较同一层级的节点。跨层移动的情况极少,直接处理为删除和新增
  2. 组件类型比较:两个不同类型的组件会生成不同的树。类型改变时直接销毁旧组件并创建新组件
  3. key 优化:通过 key 标识节点,明确哪些节点可以复用、哪些需要移动

这些策略放弃了”最优解”(如跨层复用),但在实际业务场景中(跨层移动极少),将树比较的复杂度从 O(n³) 降低到 O(n),在性能和实现复杂度之间取得了良好平衡。

单节点 Diff

当新的子节点只有一个时(如只有一个子元素或文本节点),React 会调用 reconcileSingleElement 进行比较。

packages/react-reconciler/src/ReactChildFiber.js
// 简化后的核心逻辑
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key
let child = currentFirstChild
// 遍历 current 的子节点,查找可复用的节点
while (child !== null) {
if (child.key === key) {
const elementType = element.type
// 特殊处理 Fragment
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling)
const existing = useFiber(child, element.props.children)
existing.return = returnFiber
return existing
}
} else {
// 普通 element:比较 elementType
if (
child.elementType === elementType ||
// 懒加载组件的特殊处理
(enableLazyElements &&
typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling)
const existing = useFiber(child, element.props)
existing.return = returnFiber
return existing
}
}
// key 相同但 type 不同,删除所有剩余旧节点(不同类型无法复用 DOM)
deleteRemainingChildren(returnFiber, child)
break
} else {
// key 不同,删除当前节点,继续遍历兄弟节点
deleteChild(returnFiber, child)
}
child = child.sibling
}
// 未找到可复用节点,创建新的 Fiber 节点
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key
)
created.return = returnFiber
return created
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes)
created.return = returnFiber
return created
}
}

复用条件key 相同且 elementType 匹配。

例如,<div key="1" /> 更新为 <span key="1" /> 时,key 相同但 elementType 不同,旧的 div 会被彻底删除(而非标记更新),然后创建新的 span。如果 key 不同,则直接认为是新节点。

注意:真实源码中,当找到匹配的节点时,会调用 deleteRemainingChildren(returnFiber, child.sibling) 删除所有兄弟节点,而非只删除当前节点。这是因为单节点场景下,一旦找到匹配,其他兄弟节点都应该被删除。

多节点 Diff

多节点 Diff 是最复杂的场景。React 通过两轮遍历处理数组差异:

packages/react-reconciler/src/ReactChildFiber.js
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes
): Fiber | null {
let resultingFirstChild: Fiber | null = null
let previousNewFiber: Fiber | null = null
let oldFiber = currentFirstChild
let lastPlacedIndex = 0
let newIdx = 0
let nextOldFiber = null
// 第一轮遍历:处理更新的节点
// 从左到右按 index 比较,直到遇到 key 不匹配的节点
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
// 旧节点的 index 已超过新节点 index
nextOldFiber = oldFiber
oldFiber = null
} else {
nextOldFiber = oldFiber.sibling
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
)
if (newFiber === null) {
// key 不匹配,跳出第一轮遍历
if (oldFiber === null) {
oldFiber = nextOldFiber
}
break
}
// 处理新节点复用旧节点的情况
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 匹配了 slot 但未复用 fiber,删除旧节点
deleteChild(returnFiber, oldFiber)
}
}
// placeChild 会更新 lastPlacedIndex,并判断是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
// 构建新的 Fiber 链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
oldFiber = nextOldFiber
}
// 第二轮遍历:处理剩余节点
if (newIdx === newChildren.length) {
// 新节点已遍历完,删除剩余旧节点
deleteRemainingChildren(returnFiber, oldFiber)
return resultingFirstChild
}
if (oldFiber === null) {
// 旧节点已遍历完,剩余节点作为新节点插入
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes)
if (newFiber === null) {
continue
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
return resultingFirstChild
}
// 新旧节点都有剩余,将旧节点转为 Map(key → Fiber),O(1) 查找
const existingChildren = mapRemainingChildren(oldFiber)
for (; newIdx < newChildren.length; newIdx++) {
// 从 existingChildren 中查找可复用的节点
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
)
if (newFiber !== null) {
if (shouldTrackSideEffects) {
const currentFiber = newFiber.alternate
if (currentFiber !== null) {
// 复用了旧 fiber,从 Map 中删除(避免重复复用)
existingChildren.delete(
currentFiber.key === null ? newIdx : currentFiber.key
)
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
}
}
// 删除未被复用的旧节点
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child))
}
return resultingFirstChild
}
// 放置子节点:判断节点是否需要移动,并更新 lastPlacedIndex
// 返回值:更新后的 lastPlacedIndex
function placeChild(
newFiber: Fiber, // 新创建或复用的 Fiber 节点
lastPlacedIndex: number, // 上一个不需要移动的节点的 index
newIndex: number, // 当前节点的新 index
): number {
// 设置新 fiber 的 index
newFiber.index = newIndex
if (!shouldTrackSideEffects) {
// SSR hydration 场景:不需要追踪副作用
// 标记为 Forked,表示这是列表中的节点(用于 useId 算法)
newFiber.flags |= Forked
return lastPlacedIndex
}
const current = newFiber.alternate
if (current !== null) {
// 复用了旧节点,检查是否需要移动
const oldIndex = current.index
if (oldIndex < lastPlacedIndex) {
// 旧 index 小于 lastPlacedIndex,需要移动
// 例如:A(0), B(1), C(2) → A, C, B
// A: lastPlacedIndex = 0
// C: oldIndex=2 > 0,不移动,lastPlacedIndex = 2
// B: oldIndex=1 < 2,需要移动
newFiber.flags |= Placement | PlacementDEV
return lastPlacedIndex
} else {
// 不需要移动,更新 lastPlacedIndex 为当前 index
return oldIndex
}
} else {
// 新插入的节点,标记为 Placement
newFiber.flags |= Placement | PlacementDEV
return lastPlacedIndex
}
}

关键逻辑解析

  1. 第一轮遍历:按 index 从左到右比较

    • 调用 updateSlot 比较 keytype
    • 若返回 nullkey 不匹配),跳出第一轮遍历
    • 若匹配但未复用 Fiber(newFiber.alternate === null),删除旧节点
  2. 第二轮遍历:处理剩余节点

    • mapRemainingChildren 将剩余旧节点转为 key → Fiber 的 Map,O(1) 查找
    • 调用 updateFromMap 从 Map 中查找可复用的节点
    • 若复用了旧 Fiber(newFiber.alternate !== null),从 Map 中删除(避免重复复用)
  3. 移动判断placeChild 函数

    • 返回更新后的 lastPlacedIndex
    • 若旧节点的 index 小于 lastPlacedIndex,说明需要移动,标记 Placement
    • 例如:旧节点顺序是 A(0), B(1), C(2),新顺序是 A, C, B
      • A 复用,旧 index = 0,lastPlacedIndex = 0
      • C 复用,旧 index = 2 > 0,不移动,lastPlacedIndex = 2
      • B 复用,旧 index = 1 < 2,需要移动

key 的作用

key 是 React 优化列表渲染的重要手段:

// 没有 key 或使用 index 作为 key
{items.map((item, index) =>
<Item key={index} data={item} />
)}
// 使用稳定的 key
{items.map(item =>
<Item key={item.id} data={item} />
)}

使用 index 作为 key 的问题

当列表发生插入/删除操作时,若组件类型相同,React 会按 index 位置复用 Fiber 节点,导致 state 与数据错乱。例如:

  • 旧列表:A (index=0), B (index=1), C (index=2) (每个 Item 组件有独立输入框 state)

  • 在开头插入 X:X (index=0), A (index=1), B (index=2), C (index=3)

  • Diff 过程

    • 位置 0:旧 Fiber(A) vs 新数据(X) → key 都是 0,type 相同 → 复用 Fiber
    • 位置 1:旧 Fiber(B) vs 新数据(A) → key 都是 1,type 相同 → 复用 Fiber
    • …以此类推
  • 结果:原 A 的 Fiber(含输入框 state)被复用到 X 组件,导致 X 显示了 A 的输入框值,state 与数据彻底错乱。

关键点:通常情况下列表项的组件类型都是相同的(最常见场景),此时使用 index 作为 key 就会导致 state 错乱。只有当组件类型不同时,React 才会创建新 Fiber 避免 state 错乱,但这种情况在列表渲染中极少出现;且即便如此,index 作为 key 也违背了“通过 key 标识节点唯一性”的设计初衷,并非最佳实践。

使用稳定的 id 作为 key

  • 旧列表:A(id=1), B(id=2), C(id=3)
  • 在开头插入 X:X(id=4), A(id=1), B(id=2), C(id=3)
  • React 通过 key 识别出 A、B、C 是旧节点,X 是新节点,只需插入 X,其他节点复用
  • 结果:A、B、C 的 Fiber 节点被正确复用,state 保持正确(如输入框值不会错乱)

Commit 阶段

前文中我们提到,Commit 阶段是不可中断的同步执行阶段。接下来,让我们结合源码深入理解其工作流程。

packages/react-reconciler/src/ReactFiberWorkLoop.js
// 精简后的核心流程
function commitRoot(
root: FiberRoot,
finishedWork: Fiber, // workInProgress 树的根节点(FiberRoot.current.alternate)
lanes: Lanes, // 本次渲染对应的优先级 Lane 集合
// ... 其他参数
) {
// 前置处理:计算剩余 Lanes 并标记完成
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes)
remainingLanes = mergeLanes(remainingLanes, getConcurrentlyUpdatedLanes())
markRootFinished(root, lanes, remainingLanes)
// 调度 Passive Effects
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
// 通过 MessageChannel/setTimeout 在浏览器绘制后执行
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
})
}
// 第一阶段:Before Mutation
// DOM 变更前的准备,此时真实 DOM 尚未修改
// - 执行 class 组件的 getSnapshotBeforeUpdate
commitBeforeMutationEffects(root, finishedWork)
// 第二阶段:Mutation + Layout
// React 19 支持同步或通过 View Transition 异步执行
if (enableViewTransition && shouldStartViewTransition) {
// View Transition 模式:异步执行 DOM 更新
startViewTransition(
suspendedState,
root.containerInfo,
pendingTransitionTypes,
flushMutationEffects, // Mutation 阶段
flushLayoutEffects, // Layout 阶段
// ... 其他回调
)
} else {
// 同步模式:立即执行
flushMutationEffects() // DOM 操作 + 切换 current 树
flushLayoutEffects() // 副作用处理
flushSpawnedWork() // 清理工作
}
}
// Mutation 阶段详细流程
function flushMutationEffects(): void {
if (pendingEffectsStatus !== PENDING_MUTATION_PHASE) {
return
}
pendingEffectsStatus = NO_PENDING_EFFECTS
const root = pendingEffectsRoot
const finishedWork = pendingFinishedWork
// 1. 执行 DOM 操作
if (
(finishedWork.subtreeFlags & MutationMask) !== NoFlags ||
(finishedWork.flags & MutationMask) !== NoFlags
) {
// 设置执行上下文为 CommitContext
const prevExecutionContext = executionContext
executionContext |= CommitContext
try {
// 执行 DOM 操作:插入、更新、删除节点
commitMutationEffects(root, finishedWork)
} finally {
// 恢复执行上下文
executionContext = prevExecutionContext
}
}
// 2. 切换 current 树
// 在 Mutation 之后、Layout 之前切换
// 确保 componentWillUnmount 访问旧树
// componentDidMount/Update 访问新树
root.current = finishedWork
pendingEffectsStatus = PENDING_LAYOUT_PHASE
}

源码核心流程说明

  1. 前置处理:计算剩余 Lanes,调用 markRootFinished 清理本次渲染的 Lanes
  2. Passive Effects 调度:在 Before Mutation 之前调度(确保尽早排队)
  3. Before Mutation 阶段:读取 DOM 变更前状态(getSnapshotBeforeUpdate
  4. Mutation 阶段flushMutationEffects):
    • 执行 commitMutationEffects:真实 DOM 操作(插入、更新、删除节点)
    • 切换 current 树root.current = finishedWork
  5. Layout 阶段:DOM 更新后同步处理副作用(useLayoutEffectrefclass 生命周期)
  6. View Transition(React 19 新增):支持 API 驱动的视图过渡动画

:React 19 新增 View Transition 支持,DOM 操作可通过浏览器的 document.startViewTransition 异步执行(实现过渡动画),但异步执行仍保持原子性(一次性完成所有 DOM 操作,而非中断);默认仍为同步执行。

现在让我们深入探讨几个关键问题。首先是:Commit 为什么不可中断?

如果允许在 Commit 阶段中断,会导致严重的一致性问题:

  • DOM 状态不一致:例如只插入了一半的节点树,页面结构不完整
  • 用户看到中间状态:DOM 操作只执行了一部分,用户可能看到页面闪烁或布局错乱
  • 副作用执行错误ref 可能指向错误的节点,生命周期或 Hook 可能执行多次或执行时机错误

补充:即使 React 19 支持 View Transition 异步执行 DOM 更新,也只是“将整个 Mutation + Layout 阶段延后执行”,而非“中断执行过程”。DOM 操作仍会一次性、原子化完成,避免中间状态暴露给用户。

综上所述,Commit 阶段必须一次性、原子化地完成所有操作,这些中间状态都是无法接受的情况。

在 Mutation 阶段的 DOM 操作完成后,React 执行双缓冲的核心步骤(root.current = finishedWork)切换 Fiber 树,确保 current 树始终指向”已完成 DOM 挂载的 Fiber 树”,从而避免渲染闪烁。这一机制也值得我们深入探讨。

双缓冲

这一思想源于显示技术中的垂直同步 + 双缓冲,用于解决画面撕裂问题。React 将这一思想应用到 Fiber 树的渲染中,同时在内存中维护两棵 Fiber 树:

  1. current 树:当前屏幕上显示的、已挂载到 DOM 的 Fiber 树(FiberRoot.current 指向其根节点)
  2. workInProgress 树:正在内存中构建的、用于下一次渲染的 Fiber 树(无专属根指针,通过节点的 alternate 关联到 current 树)

双指针关系

// 每个 Fiber 节点都有 alternate 指针指向另一棵树中的对应节点
current.alternate === workInProgress
workInProgress.alternate === current

工作流程

// 首次渲染:current 树为空,创建 workInProgress 树
// 注:首次渲染时所有节点 alternate 均为 null(无旧树可关联)
current Tree () workInProgress Tree (构建中)
null A1 (root)
/ \
B1 C1
// 首次渲染完成:Commit 阶段切换 FiberRoot.current 指针
// 注:root.current = A1,A1.alternate = null(首次无旧树)
current Tree (首次挂载) workInProgress Tree (已完成)
A1 (root) A1 (root)
/ \ / \
B1 C1 B1 C1
// 后续更新:基于 current 树,创建新的 workInProgress 树(A2/B2/C2)
// 注:A1.alternate = A2,A2.alternate = A1
current Tree (显示中) workInProgress Tree (构建中)
A1 (root) ◄─────────────► A2 (root)
/ \ / \
B1 C1 B2 C2
/ \ / \
D1 E1 D2 E2
// 更新完成:Commit 阶段切换 FiberRoot.current 指针
// 注:root.current = A2,A2.alternate 指向 A1(旧 current)
current Tree (显示中) workInProgress Tree (已完成)
A2 (root) ◄─────────────► A1 (root)
/ \ / \
B2 C2 B1 C1
/ \ / \
D2 E2 D1 E1

下次更新时:React 基于当前 current 树的每个节点,通过 alternate 指针创建对应的 workInProgress 节点,复用已有 Fiber 节点的结构(如 typekey),仅修改有变化的属性(如 propsflags),从而提高性能。

通过双缓冲技术,用户看到的总是完整渲染好的界面——workInProgress 树在内存中构建,即使构建到一半被中断或出错,current 树仍正常显示,UI 无闪烁、不撕裂。

优先级模型(Lane Model)

前文中我们多次提到,Fiber 架构中包含调度优先级的设计。

在 React 19 中,使用 Lane 模型来管理更新的优先级。该模型是 React 实现并发渲染的核心基础设施,使 React 能够灵活管理不同来源的更新,并根据优先级进行智能调度。

设计原理:Lane 使用一个 32 位整数的不同位来表示不同的优先级(保留 1 位作为符号位),通过简单的位运算即可实现多种操作(如合并、剔除、包含检查、提取最高优先级等)。

Lane(车道)的命名妙喻:可用的 31 位相当于 31 条不同优先级的车道,高优先级的车道可以先通过。

相比旧版的 ExpirationTime 模型,Lane 模型具有以下优势

  • 位运算高效:所有优先级操作都是位运算,执行速度极快
  • 支持批量更新:可将多个不同优先级的更新合并到一个 Lanes 集合中,一次性处理
  • 支持优先级继承:子组件可以继承父组件的优先级,确保更新的一致性
  • 支持并发渲染:可灵活中断低优先级更新,优先处理高优先级更新

Lane 的定义

packages/react-reconciler/src/ReactFiberLane.js
// 以下是大幅简化后的核心逻辑,不反映真实源码的全貌
// Lane 的位定义(从低到高,优先级从高到低)
export const TotalLanes = 31
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
// 同步 Lane:最高优先级,不能被中断
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
// 连续输入 Lane:用户连续输入(如打字、滑动),250ms 超时
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
// 默认 Lane:大部分更新使用,5000ms 超时
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane =
SyncLane | InputContinuousLane | DefaultLane;
export const GestureLane: Lane = /* */ 0b0000000000000000000000001000000;
// Transition Lanes:用于并发特性(transition),14 个 Lane 支持多个并发更新
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111100000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000010000000000;
// ...一直到 TransitionLane14
export const SomeTransitionLane: Lane = TransitionLane1;
// Retry Lanes:重试
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;
// Idle Lane:空闲时执行,几乎永不超时
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
// Offscreen Lane:隐藏的组件
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;

React 定义了众多 Lane 优先级,从高到低的层次非常直观。通过上述定义,应该对各种任务的优先级有了清晰的认识。

接下来,让我们通过几个示例看看如何进行优先级的位运算:

// 合并同步更新和默认更新,通过 `|`
const merged = SyncLane | DefaultLane
// SyncLane: 0b0000000000000000000000000000010(2)
// DefaultLane: 0b0000000000000000000000000100000(32)
// merged: 0b0000000000000000000000000100010(34)
// 判断是否包含某个优先级,通过 `&`
const has = (set: Lane, sub: Lane) => (set & sub) === sub
has(merged, SyncLane) // true
has(merged, InputContinuousLane) // false
// 移除某个优先级,通过 `&` 和 `~`
const remove = (set: Lane, sub: Lane) => set & ~sub
remove(merged, SyncLane) === DefaultLane // true
// 获取最高优先级
// lanes & -lanes 可提取出最右边的 1(对应合并结果中最高优先级的那个)
const getHighest = (lane: Lane) => lane & -lane
getHighest(merged) === SyncLane // true

React 实现中,正是基于上述这几种位运算的效果,去实现各种 Lane 的操作函数的。

Lane 的生命周期应用

Lane 贯穿了 React 更新的整个流程:创建更新调度渲染提交

接下来,我们分别探讨这四个阶段。

创建更新:分配 Lane

当调用 setStateuseReducer 或触发 React 19 的 Action 时,React 需要决定这个更新属于哪个 Lane。

这就像确定本轮发车应该走哪条车道——快车道还是慢车道。

Lane 是如何确定的?

  • 同步事件(如 onClick 直接调用 setState

    • Legacy 模式:分配 SyncLane(同步不可中断)
    • Concurrent 模式:根据事件类型分配
      • 用户交互事件(点击、输入):SyncLaneInputContinuousLane
      • 其他事件:DefaultLane(可被高优先级打断)
  • startTransition

    • TransitionLanes 中(14 个)分配一个空闲的 Lane
  • React 19 Actions

    • useActionState 或 Form Action 中,dispatch 自动分配 TransitionLanes
    • Action 中调用 flushSync,则临时升级为 SyncLane
  • useOptimistic

    • 乐观更新(立即显示):分配高优先级 Lane(如 SyncLane
    • 回退(如果请求失败):分配低优先级 Lane(如 TransitionLane

调度:决定何时渲染

分配 Lane 后,React 会根据 Lane 的优先级,决定是同步渲染还是并发渲染,并将任务提交给 Scheduler。

相关实现ensureRootIsScheduled(源码位置:packages/react-reconciler/src/ReactFiberRootScheduler.js

核心逻辑如下

  • 判断渲染模式

    • 若 Lane 是 SyncLaneSyncUpdateLanes,则同步渲染(调用 workLoopSync
    • 否则,并发渲染(调用 workLoopConcurrent
  • 映射到 Scheduler 优先级

    • SyncLaneImmediatePriority
    • InputContinuousLaneUserBlockingPriority
    • DefaultLaneNormalPriority
    • TransitionLanesLowPriority
    • IdleLaneIdlePriority
  • 提交调度任务

    • 调用 Scheduler 的 scheduleCallback,将协调器的 performWorkOnRoot 作为回调提交

渲染:选择下一个 Lane

这是 Lane 模型最核心的逻辑。当 React 准备渲染时,需要从 root.pendingLanes 中选择优先级最高、未被阻塞的 Lane 进行处理。

packages/react-reconciler/src/ReactFiberLane.js
export function getNextLanes(
root: FiberRoot,
wipLanes: Lanes, // 当前正在渲染的 Lanes
rootHasPendingCommit: boolean
): Lanes {
const pendingLanes = root.pendingLanes
// 1. 若没有待处理的更新,返回 NoLanes
if (pendingLanes === NoLanes) {
return NoLanes
}
let nextLanes = NoLanes
const suspendedLanes = root.suspendedLanes // 被 Suspense 挂起的 Lanes
const pingedLanes = root.pingedLanes // 被 ping 通(可恢复)的 Lanes
// 2. 优先处理非空闲的、未被挂起的 Lanes
const nonIdlePendingLanes = pendingLanes & NonIdleLanes
if (nonIdlePendingLanes !== NoLanes) {
// 过滤掉被 Suspense 挂起且未被 ping 的 Lanes
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes
if (nonIdleUnblockedLanes !== NoLanes) {
// 提取优先级最高的 Lane(最右边的 1)
nextLanes = getHighestPriorityLane(nonIdleUnblockedLanes)
} else {
// 处理被 ping 通的 Lanes
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLane(nonIdlePingedLanes)
}
}
} else {
// 处理空闲 Lanes
const idleUnblockedLanes = pendingLanes & IdleLanes & ~suspendedLanes
if (idleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLane(idleUnblockedLanes)
}
}
// 3. 优先级抢占判断:若当前已在渲染某个 Lane,且新选出的 Lane 优先级不够高,则继续渲染当前的
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
(wipLanes & suspendedLanes) === NoLanes
) {
const nextLane = getHighestPriorityLane(nextLanes)
const wipLane = getHighestPriorityLane(wipLanes)
if (
// 检查新任务的 lane 优先级是否小于等于当前的(wip)
nextLane >= wipLane ||
// 默认优先级的更新不能打断 transition 更新
// 这条判断可缓解连续涌入的 DefaultLane 导致 TransitionLanes 任务饥饿
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
) {
// 当前渲染的优先级更高,不打断
return wipLanes
}
}
return nextLanes
}

在 React 19 中,由于 Actions 的普及,pendingLanes 中经常包含 TransitionLanesgetNextLanes 会确保:

  • 若有用户输入(InputContinuousLane),优先渲染
  • 若无高优先级更新,再渲染 Transition Lane(Actions 带来的更新)
  • Transition Lane 渲染过程中来了高优先级更新,立即中断,先处理高优先级

提交:清除 Lane

渲染提交(Commit)完成后,需要将已处理的 Lane 从 root.pendingLanes 中移除,并清理相关的辅助数据结构。React 源码中通过 markRootFinished 函数统一、高效地完成这个逻辑。

packages/react-reconciler/src/ReactFiberWorkLoop.js
// 简化后的核心逻辑
function commitRoot(
root: FiberRoot,
finishedWork: Fiber,
lanes: Lanes,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
// ... 其他参数
) {
// 核心逻辑 1:计算剩余未完成的 Lanes
// 1. 从 finishedWork 树中收集所有未完成的 Lanes
// 合并当前节点的 lanes 和子树的 childLanes
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes)
pendingEffectsRemainingLanes = remainingLanes
// 2. 处理渲染期间的并发更新
// 获取在 Render 阶段被并发事件更新的 Lanes
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes()
// 这些 Lanes 不能被标记为”完成”,需合并到 remainingLanes 中
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes)
// 3. (可选)处理手势相关的 Lanes
if (enableGestureTransition && root.pendingGestures === null) {
if (enableProfilerTimer && (remainingLanes & GestureLane) !== NoLanes) {
clearGestureUpdates()
}
remainingLanes &= ~GestureLane
}
// 核心逻辑 2:标记 Root 为完成
// 调用 markRootFinished 统一处理 Lanes 的清除
markRootFinished(
root,
lanes, // 本次渲染处理的 Lanes
remainingLanes, // 剩余未完成的 Lanes
spawnedLane,
updatedLanes,
suspendedRetryLanes,
)
// ...
}
// packages/react-reconciler/src/ReactFiberLane.js
function markRootFinished(
root: FiberRoot,
finishedLanes: Lanes,
remainingLanes: Lanes,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
) {
// 1. 计算并更新 pendingLanes
const previouslyPendingLanes = root.pendingLanes
// 计算”不再待处理的 Lanes”:之前待处理的 - 剩余的
const noLongerPendingLanes = previouslyPendingLanes & ~remainingLanes
// 直接将 pendingLanes 设置为剩余的 Lanes(最核心的一步)
root.pendingLanes = remainingLanes
// 2. 重置各种临时标志位
root.suspendedLanes = NoLanes // 清空被 Suspense 挂起的 Lanes
root.pingedLanes = NoLanes // 清空被 ping 通的 Lanes
root.warmLanes = NoLanes // 清空预热 Lanes
if (enableDefaultTransitionIndicator) {
root.indicatorLanes &= remainingLanes // 保留剩余 Lanes 的指示器
}
root.expiredLanes &= remainingLanes // 保留剩余 Lanes 的过期标记
root.entangledLanes &= remainingLanes // 保留剩余 Lanes 的纠缠关系
root.errorRecoveryDisabledLanes &= remainingLanes // 保留剩余 Lanes 的错误恢复标记
root.shellSuspendCounter = 0 // 重置 Shell 挂起计数器
// 3. 清理辅助数据结构
const entanglements = root.entanglements
const expirationTimes = root.expirationTimes
const hiddenUpdates = root.hiddenUpdates
// 遍历所有”不再待处理的 Lanes”,清理对应的数据
let lanes = noLongerPendingLanes
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes) // 取出任意一个 Lane 的索引
const lane = 1 << index
// 清理该 Lane 的纠缠关系
entanglements[index] = NoLanes
// 清理该 Lane 的过期时间
expirationTimes[index] = NoTimestamp
// 清理该 Lane 的隐藏更新(Offscreen 组件相关)
const hiddenUpdatesForLane = hiddenUpdates[index]
if (hiddenUpdatesForLane !== null) {
hiddenUpdates[index] = null
// 将隐藏更新的 Lane 从 OffscreenLane 中移除,使其变为普通更新
for (let i = 0; i < hiddenUpdatesForLane.length; i++) {
const update = hiddenUpdatesForLane[i]
if (update !== null) {
update.lane &= ~OffscreenLane
}
}
}
// 从 lanes 中移除已处理的 Lane
lanes &= ~lane
}
// 4. 处理衍生的 Deferred Lane
if (spawnedLane !== NoLane) {
markSpawnedDeferredLane(
root,
spawnedLane,
NoLanes, // 本次渲染未挂起,无需纠缠父任务
)
}
// 5. 处理 Suspense 重试 Lanes
if (
suspendedRetryLanes !== NoLanes &&
updatedLanes === NoLanes && // 渲染期间无新更新
!(disableLegacyMode && root.tag === LegacyRoot)
) {
// 计算”新生成的重试 Lanes”
const freshlySpawnedRetryLanes =
suspendedRetryLanes &
~(previouslyPendingLanes & ~finishedLanes)
// 将这些 Lanes 标记为挂起,下次渲染时直接进入预渲染模式
root.suspendedLanes |= freshlySpawnedRetryLanes
}
}

Commit 阶段清除 Lane 的核心逻辑可概括为

  • 计算剩余 Lanes:从 finishedWork.lanesfinishedWork.childLanesconcurrentlyUpdatedLanes 中合并出剩余未完成的 Lanes
  • 更新 pendingLanes:直接将 root.pendingLanes 设置为 remainingLanes,一步完成核心清除
  • 重置标志位:清空 suspendedLanespingedLanes 等临时标志位,保留剩余 Lanes 的相关标记
  • 清理辅助数据:遍历 noLongerPendingLanes,清理 entanglementsexpirationTimeshiddenUpdates 等辅助数据结构
  • 处理特殊场景:处理衍生的 Deferred Lane 和 Suspense 重试 Lane

饥饿问题

在 React 并发渲染模型中,不同优先级的更新会被差异化调度:高优先级更新(如用户输入、点击)可以打断低优先级更新(如数据渲染、表单提交),优先响应用户操作。但这也带来了一个经典的调度问题——更新饥饿:若高优先级更新持续涌入,低优先级更新可能会被无限期打断,永远无法完成执行。

React 通过 Lane 老化(Lane Aging) 核心机制,配合 调度规则的兜底保护,彻底解决了这个问题,确保所有更新最终都能被执行。

Lane 老化(Lane Aging)

核心逻辑:更新的优先级并非固定不变。低优先级更新的等待时间越长,其执行优先级会被动态提升,最终会被标记为”过期任务”强制同步执行,从根本上避免饥饿。

1. 核心实现:markStarvedLanesAsExpired

Lane 老化的全部核心逻辑都在 markStarvedLanesAsExpired 函数中(源码位置:packages/react-reconciler/src/ReactFiberLane.js)。其核心执行流程如下:

// 精简后的核心逻辑
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes // 所有待处理的更新 Lane
const suspendedLanes = root.suspendedLanes // 被 Suspense 挂起的 Lane
const pingedLanes = root.pingedLanes // 被 ping 通、可恢复执行的 Lane
const expirationTimes = root.expirationTimes // 每个 Lane 对应的过期时间数组
// 遍历所有待处理的 Lane(默认排除重试 Lane)
let lanes = enableRetryLaneExpiration
? pendingLanes
: pendingLanes & ~RetryLanes
while (lanes > 0) {
// 取出单个待处理的 Lane
const index = pickArbitraryLaneIndex(lanes)
const lane = 1 << index
const expirationTime = expirationTimes[index]
// 分支 1:该 Lane 还未设置过期时间
if (expirationTime === NoTimestamp) {
// 仅对"未被挂起"或"已被 ping 通"的 Lane 设置过期时间
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// 根据 Lane 的优先级,计算对应的过期时间(优先级越低,过期时间越长)
expirationTimes[index] = computeExpirationTime(lane, currentTime)
}
}
// 分支 2:该 Lane 已达到过期时间,标记为过期
else if (expirationTime <= currentTime) {
root.expiredLanes |= lane
}
// 移除已处理的 Lane,继续遍历
lanes &= ~lane
}
}

核心作用

  • 初始化过期时间:为每个待执行的更新 Lane,根据其优先级设置对应的过期时间
  • 标记过期 Lane:若一个 Lane 的等待时间超过其过期时间,将其加入 root.expiredLanes,标记为需要强制执行的过期任务

2. 触发时机:微任务阶段的前置检查

markStarvedLanesAsExpired 并非在渲染过程中调用,而是在 scheduleTaskForRootDuringMicrotask 函数中被调用(源码位置:packages/react-reconciler/src/ReactFiberRootScheduler.js)。

触发时机

  • 在每次正式调度渲染任务前的微任务阶段执行
  • 调度器每次让出主线程后,重新调度任务时也会执行
  • 确保在每次渲染前,都能及时检查并标记等待超时的低优先级更新

3. 过期 Lane 的执行保障

一旦一个 Lane 被标记到 root.expiredLanes 中,React 会对其做特殊处理:

  • 强制使用同步渲染模式(workLoopSync)执行,不再遵循时间片调度,不会被 shouldYield() 中断
  • 优先于所有未过期的更新执行,确保超时的低优先级更新能尽快完成

辅助防饥饿的调度兜底规则

除了核心的 Lane 老化机制,React 在前文解释过的 getNextLanes 函数(调度优先级决策的核心入口)中,还设置了兜底的调度规则,进一步避免低优先级更新被无限打断:

// getNextLanes 片段
if (
nextLane >= wipLane ||
// 关键:Default 优先级更新(普通 setState)不打断正在执行的 Transition 更新
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
) {
// 继续执行当前任务,不中断
return wipLanes
}

Default 不打断 Transition:普通的状态更新不会打断正在执行的 Transition 任务,这对 React 19 中大量使用 TransitionLanes 的 Actions 体系至关重要。

:在 React 19 中,useActionState、Form Actions 等特性大量使用 TransitionLanes

并发特性(Concurrent Features)

React 16 引入 Fiber 架构,React 18 正式发布并发特性,React 19 进一步优化和普及(如 Actions 体系)。这些特性的底层都依赖于前文所讲的 Fiber 可中断遍历、Lane 优先级模型和时间片调度。

自动合批(Automatic Batching)

自动合批的作用是将同一上下文的多个状态更新收集为同一批渲染,减少不必要的 DOM 操作和重渲染。

在较早的版本中,React 仅在 React 事件处理函数(如 onClick)中自动合批,在 setTimeoutPromise、原生事件中不会批处理。

而到了 React 18,已支持所有上下文自动合批。

示例

function handleClick() {
setCount(c => c + 1) // 收集
setText('clicked') // 收集
}
// React 18+ 自动合批
setTimeout(() => {
setCount(c => c + 1) // 收集
setText('clicked') // 收集
}, 0)
async function fetchData() {
const res = await api()
setData(res) // 收集
setLoading(false) // 收集
}

底层实现

React 为所有状态更新场景(无论 React 事件、PromisesetTimeout 等上下文)建立统一的“批处理窗口”,在该窗口期内到达的更新,都会被收集到更新队列中,不会立即触发渲染。当批处理窗口结束(基于事件循环机制)时,React 调度刷新任务统一处理队列中的所有更新。

Transitions

Transition 用于将更新标记为“非紧急”(TransitionLanes),允许被高优先级更新打断,避免阻塞用户交互。

Transition 更新具有以下特性

  • 可中断:高优先级更新(如用户输入)可以打断 Transition
  • 无闪烁:Transition 期间会显示旧状态,不会显示中间加载态(除非配合 Suspense)
  • 最终一致性:通过 Lane 老化机制和调度规则,保证 Transition 最终一定会执行

底层实现

Transitions 更新分配 TransitionLanes(14 个 Lane 通道,支持多个并发 Transition)。在 getNextLanes 中,TransitionLanes 优先级低于 InputContinuousLaneDefaultLane。不过源码实现上做了优化,Transition 一旦开始,就不会被普通更新的 DefaultLane 打断,缓解了饥饿问题。

Suspense

声明式地处理组件加载状态,让组件可以“等待”数据加载完成后再渲染。

可用于

  • 组件懒加载(React.lazy + Suspense
  • 数据获取:配合 RelayReact QuerySWR 等数据获取库,声明式等待数据
  • 路由懒加载:React Router 6+ 配合 Suspense 实现路由级别的加载状态

底层原理

组件抛出 Promise 时,React 会将其对应的 Lane 加入 root.suspendedLanesPromiseresolve 后,调用 pingSuspendedRoot,将 Lane 加入 root.pingedLanesgetNextLanes 会优先处理 pingedLanes,恢复被挂起的渲染。

示例

import { Suspense, lazy } from 'react'
const LazyComponent = lazy(() => import('./LazyComponent'))
const DataComponent = lazy(() => import('./DataComponent'))
function App() {
return (
<div>
<Suspense fallback={<div>Loading component...</div>}>
<LazyComponent />
</Suspense>
<Suspense fallback={<div>Loading data...</div>}>
<DataComponent />
</Suspense>
</div>
)
}

Hook: useId

此 Hook 用于生成唯一、稳定的 ID,兼容服务端渲染(SSR)和并发渲染。

解决的问题

  • 避免 SSR 中客户端和服务端 ID 不一致导致的 hydration 错误
  • 避免并发渲染中 ID 生成顺序不确定导致的问题

示例

import { useId } from 'react'
function Form() {
const id = useId()
return (
<div>
<label htmlFor={`${id}-name`}>Name</label>
<input id={`${id}-name`} type="text" />
<label htmlFor={`${id}-email`}>Email</label>
<input id={`${id}-email`} type="email" />
</div>
)
}

Hook: useTransition

手动控制某个更新为 Transition,返回 startTransition 函数和 isPending 状态。

底层原理

startTransition 内部会将更新分配到 TransitionLanesisPending 会在 Transition 期间返回 true,用于显示加载状态。

典型使用场景

  • 搜索框输入:将搜索结果更新标记为 Transition,避免阻塞用户输入
  • 标签页切换:将标签页内容更新标记为 Transition
  • 表单提交:React 19 中 Actions 内部自动使用,但手动控制时仍可用

示例

function Search() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
const searchText = e.target.value
// 立即更新输入框(高优先级)
setQuery(searchText)
// 低优先级更新,可被打断
startTransition(() => {
setResults(filterList(searchText))
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <div>Searching...</div>}
<HeavyResults results={results} />
</div>
)
}

Hook: useDeferredValue

延迟更新某个值,让该值的更新作为 Transition 执行。

同样作为 Transition 执行,它与 useTransition 的区别在于:useTransition 控制更新函数,而 useDeferredValue 控制更新的值,适用于无法直接控制更新函数的场景(如第三方库返回的值)。

底层原理

延迟后的值的更新会被分配到 TransitionLanes,旧值会在 Transition 期间继续使用,避免闪烁。

典型用例

  • 列表渲染(延迟更新列表数据,避免阻塞用户输入)
  • 第三方库集成(延迟第三方库返回的值的更新)

示例

function App() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<Suspense fallback={<h2>Loading...</h2>}>
<HeavyResults query={deferredQuery} />
</Suspense>
</>
)
}

Hook: useSyncExternalStore

同步外部状态,避免并发渲染导致的”状态撕裂”。

在并发渲染中,外部状态可能在渲染过程中发生变化,导致组件不同部分使用不同的状态值(状态撕裂)。该 Hook 确保组件在一次渲染中使用的外部状态是一致的。

底层原理

结合 Fiber 和 Lane 模型,在组件 mount 时订阅外部状态,getSnapshot 在每次 render 时调用获取当前状态。若外部状态在渲染过程中发生变化,会强制重新渲染,配合 SyncLane 确保外部状态更新是同步的,避免撕裂。

使用场景

  • 集成 ReduxMobX 等状态管理库
  • 订阅浏览器 API(如 window.innerWidthlocalStorage
  • 自定义外部状态管理

示例

import { useSyncExternalStore } from 'react'
// 订阅浏览器窗口宽度
function useWindowWidth() {
return useSyncExternalStore(
callback => {
window.addEventListener('resize', callback)
return () => window.removeEventListener('resize', callback)
},
() => window.innerWidth,
() => 1024 // 服务端渲染的 fallback 值
)
}
function Component() {
const width = useWindowWidth()
return <div>Window width: {width}</div>
}

Hook: useInsertionEffect

这是为 CSS-in-JS 库作者准备的 Hook。此 Hook 可以在 layout Effects 触发前,将元素插入 DOM,用于 CSS-in-JS 库注入样式。

使用此 Hook 让样式在 DOM 渲染前就注入,可避免在 useLayoutEffect 中注入样式导致的布局抖动。

工作顺序

  1. useInsertionEffect(注入样式)
  2. DOM Mutation 阶段
  3. useLayoutEffect(读取布局、同步修改 DOM)
  4. 浏览器绘制
  5. useEffect(异步副作用)

使用场景:CSS-in-JS 库(如 styled-components、Emotion)内部使用,业务代码中基本不直接使用。

示例

import { useInsertionEffect } from 'react'
function useCSS(styles) {
useInsertionEffect(() => {
// 注入样式到 <style> 标签
const style = document.createElement('style')
style.textContent = styles
document.head.appendChild(style)
return () => document.head.removeChild(style)
}, [styles])
}

三、Hooks

Hooks 是 React 16.8 引入的特性。Hooks 的引入,彻底改变了使用 React 开发应用的方式。

在此之前,类组件存在几个明显问题:

逻辑复用困难:复用状态逻辑只能通过高阶组件(HOC)或 render props,导致组件嵌套层级加深,形成”wrapper hell”,调试困难。

相关逻辑分散:组件中的相同生命周期函数(如 componentDidMountcomponentDidUpdate)往往包含不相关的逻辑,难以拆分。例如,数据获取和事件监听都在 componentDidMount 中,清理却要分别在 componentWillUnmount 中处理。

class 的复杂性:需要理解 this 绑定、构造函数、生命周期方法等概念,对新手不友好。

Hooks 的引入很好地解决了这些问题:

逻辑复用简单:通过自定义 Hooks 复用状态逻辑,无需改变组件层级结构。例如 useFormuseWindowSize 等自定义 Hooks 可以在不同组件间轻松复用。

相关逻辑集中:通过 useStateuseEffect 等 Hooks,相关逻辑可以组织在一起,而不是分散在不同生命周期方法中。例如,数据获取和清理逻辑可以在同一个 useEffect 中完成。

函数式风格:无需 class 和 this,代码更简洁,React 组件回归纯函数本质,更容易理解和测试。

用一个例子感受下:

// 类组件:逻辑分散在不同生命周期方法中
class WindowSize extends React.Component {
constructor(props) {
super(props)
this.state = { width: window.innerWidth }
this.handleResize = this.handleResize.bind(this) // this 绑定
}
componentDidMount() {
window.addEventListener('resize', this.handleResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize) // 清理逻辑分散
}
handleResize() {
this.setState({ width: window.innerWidth })
}
render() {
return <div>Width: {this.state.width}</div>
}
}
// Hooks 版本:相关逻辑集中在一起
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // 清理在一起
}, [])
return <div>Width: {width}</div>
}
// 进一步:提取为自定义 Hook,逻辑复用
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return width
}
// 任何组件都可以轻松复用
function Component1() {
const width = useWindowWidth()
return <div>Width: {width}</div>
}

这个例子充分展示了 Hooks 的优势:

  • 类组件中 addEventListenerremoveEventListener 分散在不同生命周期方法中,Hooks 版本集中在一起
  • 类组件需要手动绑定 this,Hooks 版本无需关心 this
  • 逻辑提取为 useWindowWidth 后,其他组件可以直接复用,无需改变组件层级

接下来我们尝试从源码层面,来研究 Hooks 的实现原理。


Hooks 的底层存储形式

首先,前文讲 Fiber 的时候,已经给出了 Fiber 的关键数据结构,其中的 memoizedState 字段,在函数组件中,就是 Hooks 相关信息的挂载处。

interface Fiber {
memoizedState: Hook | null // 函数组件:指向第一个 Hook 节点
// 类组件:存储组件的 state(注意区分)
// ...
}

然后,我们再看看 Hook 的结构:

// `packages/react-reconciler/src/ReactFiberHooks.js`
type Hook = {
memoizedState: any, // 当前状态值(不同 Hook 用途不同)
// - useState/useReducer: 存储最新状态
// - useEffect: 存储 Effect 对象(含回调、依赖等)
// - useMemo/useCallback: 存储缓存值/函数
baseState: any, // 基础状态(用于优先级调度:低优先级更新基于此状态计算)
baseQueue: Update<any, any> | null, // 基础更新队列(存储所有未处理的更新)
queue: UpdateQueue<any, any> | null, // 当前更新队列(存储本次渲染的更新)
next: Hook | null, // 指向下一个 Hook(形成单向链表)
}

这个 Hook 结构就是函数组件中各种 useXXX 的 Hooks 调用后产生的,上面存储了当前状态值、更新队列、基础状态等信息。

可以注意到,Hook 中有个 next 指针,这是用来做什么的?

React 通过调用顺序来标识不同的 Hook,因此使用一个单向链表就可以存储组件内的多个 Hook 的信息了,next 指针就是用来指向下一个链表节点的(后续的源码分析中可以看到更多细节)。

如下组件中多个 useXXX 调用:

function HookDemo() {
const [count, setCount] = useState(0) // Hook1
useEffect(() => {/**/}, []) // Hook2
const value = useMemo(() => {/**/}, []) // Hook3
//...
}

就可以以一个 Hook 链表存储到 Fiber 的 memoizedState 字段上了:

Fiber.memoizedState → Hook1 (useState) → Hook2 (useEffect) → Hook3 (useMemo) → null

首次渲染(mount)时,React 会创建新的 Hook 节点并添加到链表。更新(update)时,React 会按顺序复用旧 Fiber 上的 Hook 节点,仅更新状态。

相关机制后续章节会通过源码解读逐步揭示


Hooks 的调用机制

React 通过全局变量跟踪当前渲染的 Fiber 和当前 Hook:

packages/react-reconciler/src/ReactFiberHooks.js
// 全局变量
// 正在渲染的 workInProgress Fiber 节点(所有 Hook 调用的“宿主”)
let currentlyRenderingFiber: Fiber = (null: any)
// 旧 Fiber(current 树)上的对应 Hook(仅 update 阶段有值,用于复用状态)
let currentHook: Hook | null = null
// 正在构建的 workInProgress 树的 Hook 链表(当前正在处理的 Hook 节点)
let workInProgressHook: Hook | null = null

其中,当前渲染的 Fiber,即源码中的 currentlyRenderingFiber,在渲染函数组件的入口函数(renderWithHooks)中设置,调用链路:

renderWithHooks

设置全局变量(currentlyRenderingFiber)

是否首次渲染

HooksDispatcherOnMount

执行组件代码

mountWorkInProgressHook

HooksDispatcherOnUpdate

执行组件代码

updateWorkInProgressHook

继续渲染...

我们来看看这个 renderWithHooks(渲染函数组件的入口函数)的实现:

packages/react-reconciler/src/ReactFiberHooks.js
// 标记本次渲染是否有 render 阶段的更新(如 render 中调用 setState)
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// 渲染函数组件的入口函数
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
// 设置当前渲染的 Fiber
currentlyRenderingFiber = workInProgress
// 重置 workInProgress Fiber 的 Hooks 状态(清空旧链表,准备构建新的)
workInProgress.memoizedState = null
// 省略代码...
// 区分 mount / update 设置 Dispatcher
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate
// 省略代码...
// 执行函数组件(内部会调用 useState、useEffect 等)
let children = Component(props, secondArg)
// 检查是否有 render 阶段的更新(如 setState 在 render 中调用)
// 如果有则使用 renderWithHooksAgain 重新渲染
if (didScheduleRenderPhaseUpdateDuringThisPass) {
// renderWithHooksAgain 里面会:
// 1. 重新设置 currentlyRenderingFiber
// 2. 重置 currentHook、workInProgressHook 为 null,从头开始
// 3. 设置为 re-render 模式的 Dispatcher
// 4. 重新执行 Component 逻辑
// 如果过程中,又有 render 阶段的更新,则其中 2、3、4 步在一个循环中反复执行,直到没有更新
children = renderWithHooksAgain(workInProgress, Component, props, secondArg)
}
// 完成 Hooks 渲染
finishRenderingHooks(current, workInProgress)
return children
}
// 完成 Hooks 渲染
function finishRenderingHooks(
current: Fiber | null,
workInProgress: Fiber,
): void {
// 检查 Hook 数量是否正确(防止提前 return 导致数量不匹配)
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null
// 恢复 Dispatcher 为 ContextOnlyDispatcher
// 此时在 render 外调用 Hooks 会报错
ReactSharedInternals.H = ContextOnlyDispatcher
// 略...
// 清理全局变量
currentlyRenderingFiber = null
currentHook = null
workInProgressHook = null
// Hook 数量不匹配时抛出错误
if (didRenderTooFewHooks) {
throw new Error(/*略*/
}
// 检查 Context 是否发生变化
if (current !== null && !checkIfWorkInProgressReceivedUpdate()) {
const currentDependencies = current.dependencies
if (
currentDependencies !== null &&
checkIfContextChanged(currentDependencies)
) {
markWorkInProgressReceivedUpdate()
}
}
}

综上,整个 Hook 的处理过程,大致如下:

  1. 准备阶段:设置全局变量 currentlyRenderingFiber,让 Hook 调用能访问当前 Fiber,重置 workInProgress Fiber 的 Hooks 状态和全局 Hook 跟踪变量
  2. 选择 Dispatcher:mount 用 HooksDispatcherOnMount,update 用 HooksDispatcherOnUpdate
  3. 执行组件:调用组件函数 Component,内部按顺序调用 Hooks
  4. 处理 render 阶段更新:如果有 render 中的 setState,用 renderWithHooksAgain 重新渲染
  5. 清理finishRenderingHooks 重置全局变量,恢复 Dispatcher,检查 Hook 数量

其中,组件函数 Component 内部调用 Hooks,Hooks 在执行的时候,通过上述的全局变量,实现与 Fiber 的联系,创建或更新 Fiber 上的 Hook 链表。关键实现函数:

// 创建 Hook 数据结构并加入链表
function mountWorkInProgressHook(): Hook {
// 创建新的 Hook 对象
const hook: Hook = {
memoizedState: null, // 当前状态值
baseState: null, // 基础状态(用于优先级计算)
baseQueue: null, // 基础更新队列
queue: null, // 更新队列
next: null, // 指向下一个 Hook
};
// 将 Hook 加入 Fiber 的 Hook 链表
if (workInProgressHook === null) {
// 如果是第一个 Hook:设置为 Fiber.memoizedState 的入口,同时更新全局指针
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 如果是后续的 Hook:追加到链表末尾
workInProgressHook = workInProgressHook.next = hook;
}
// 返回新创建的 Hook,同时更新全局 workInProgressHook 指针
return workInProgressHook;
}
// Update 阶段:获取或创建当前 Hook
function updateWorkInProgressHook(): Hook {
// 1. 获取 current 树上的 Hook
let nextCurrentHook: null | Hook;
if (currentHook === null) {
// 首次渲染该组件或首次调用此 Hook
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current?.memoizedState ?? null;
} else {
// 后续 Hook:移动到下一个
nextCurrentHook = currentHook.next;
}
// 2. 获取 workInProgress 树上的 Hook
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
// 首次调用此 Hook
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 后续 Hook:移动到下一个
nextWorkInProgressHook = workInProgressHook.next;
}
// 3. 复用或克隆 Hook
if (nextWorkInProgressHook !== null) {
// 情况 A:workInProgress Hook 已存在(render 阶段重渲染)
// 直接复用,移动指针
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// 情况 B:workInProgress Hook 不存在,从 current Hook 克隆
// 错误检查:必须有 current Hook 可以克隆
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// 首次渲染调用了 update dispatcher(内部错误)
throw new Error(
'Update hook called on initial render. This is likely a bug in React.',
);
} else {
// Hook 数量超过上次渲染(条件调用导致)
throw new Error('Rendered more hooks than during the previous render.');
}
}
currentHook = nextCurrentHook;
// 克隆 current Hook 到 workInProgress
const newHook: Hook = {
memoizedState: currentHook.memoizedState, // 复用状态
baseState: currentHook.baseState, // 复用基础状态
baseQueue: currentHook.baseQueue, // 复用基础队列
queue: currentHook.queue, // 复用更新队列
next: null, // next 稍后设置
};
// 加入 workInProgress Hook 链表
if (workInProgressHook === null) {
// 第一个 Hook:设置到 Fiber.memoizedState
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 后续 Hook:追加到链表末尾
workInProgressHook = workInProgressHook.next = newHook;
}
}
// 返回当前 Hook,同时更新全局 workInProgressHook 指针
return workInProgressHook;
}

解读下这两个函数做的事情:

mountWorkInProgressHook 的核心逻辑很简单:

  • 创建新 Hook 对象,通过全局指针 workInProgressHook 维护链表顺序。第一个 Hook 挂载到 Fiber.memoizedState,后续 Hook 通过 next 指针追加到链表末尾。

updateWorkInProgressHook 的核心逻辑是复用或克隆:

  • 情况 A:workInProgress Hook 已存在(render 阶段重渲染时),直接复用,不创建新对象
  • 情况 B:workInProgress Hook 不存在,从 current Hook 克隆一个新对象

克隆时注意 queue 字段是直接复用的(不是拷贝),这保证了更新队列在 current 和 workInProgress 之间共享,更新不会丢失。
错误检查:当 nextCurrentHook === null 时抛出错误,这防止了条件调用 Hook 导致的 Hook 数量不一致问题,这也解释了为什么 Hooks 必须顶层调用。

至此,我们算是初步弄清楚了整个 Hooks 大致的调用脉络了。

具体到每个 Hooks,我们还需要逐个研究源码,以弄清楚以上创建、更新的过程是怎么发生的。

不过在这之前,需要先弄清楚上面源码中出现的 Dispatcher 的概念。

Dispatcher

前文提到,renderWithHooks 会根据 mount/update 设置不同的 Dispatcher:

ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate

那么 Dispatcher 是什么?为什么需要它?

一句话概括:

Dispatcher 是一个包含所有 Hook 实现的对象

React 会在不同的情景中,使用不同的 Dispatcher 实现来处理组件函数中的 Hook 调用,以实现多态。

一共有以下 四种 Dispatcher

// 1. 首次渲染时的 Dispatcher
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
// ... 其他 Hooks
}
// 2. 更新渲染时的 Dispatcher
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
// ...
}
// 3. render 阶段重渲染的 Dispatcher(renderWithHooksAgain 使用)
const HooksDispatcherOnRerender = {
useState: rerenderState, // 与 update 相同
useEffect: updateEffect,
// ...
}
// 4. render 外调用 Hooks 的 Dispatcher(一律直接抛错)
const ContextOnlyDispatcher = {
useState: throwInvalidHookError // 调用就抛错,
useEffect: throwInvalidHookError,
// ...
}

这些 Dispatcher,会在以下情景切换:

  1. renderWithHooks 开始:根据 mount/update 切换到 HooksDispatcherOnMountHooksDispatcherOnUpdate
  2. renderWithHooksAgain:切换到 HooksDispatcherOnRerender
  3. finishRenderingHooks:恢复为 ContextOnlyDispatcher
  4. 渲染过程捕获到错误:调用 resetHooksAfterThrow 确保非渲染期间都能恢复到 ContextOnlyDispatcher

React 通过 Dispatcher,实现了以下目的:

  1. 区分 mount 和 update 逻辑

    • 根据 mount/update 切换到 HooksDispatcherOnMountHooksDispatcherOnUpdate。 例如调用 useState,前者会派发 mountState,会创建新 Hook 对象,初始化状态;而后者会派发 updateState,复用已有 Hook 对象,计算新状态
  2. 防止在 render 外调用 Hooks

    • 通过切换到 ContextOnlyDispatcher,确保 Hooks 只在 render 阶段被调用
    • 如果在事件处理函数、setTimeout 等地方调用 Hooks,会触发错误

以一个真实组件代码来理解这个过程:

function Counter() {
// renderWithHooks 设置 Dispatcher
// ↓ 此时 useState 对应 HooksDispatcherOnUpdate.useState
const [count, setCount] = useState(0)
useEffect(() => {
// setTimeout 中调用 Hooks
setTimeout(() => {
// 此时 Dispatcher 已恢复为 ContextOnlyDispatcher
// const [x] = useState(0) // 报错!
}, 1000)
}, [])
return <button onClick={() => setCount(count + 1)}>{count}</button>
}

理解了 Dispatcher 之后,我们接下来看看各个主要的 Hook 的实现,这部分逻辑都非常复杂,需要耐心分析理解。

useReducer 的实现

mount 阶段的 useReducer

mount 阶段,useReducer 使用 HooksDispatcherOnMount 派发 mountReducer

提示:useReduceruseState 的实现非常相似,先弄清楚 useReducer,后面再看 useState 更容易理解。

// useReducer 的 Mount 阶段实现
function mountReducer<S, I, A>(
reducer: (S, A) => S, // 用户自定义的 reducer 函数
initialArg: I, // 初始参数
init?: I => S, // 惰性初始化函数
): [S, Dispatch<A>] {
// 1. 创建 Hook 对象并加入链表(参考前文的解读)
const hook = mountWorkInProgressHook();
// 2. 计算初始状态
let initialState;
if (init !== undefined) {
// 有 init 函数:init(initialArg) → 惰性初始化
initialState = init(initialArg);
} else {
// 无 init 函数:直接使用 initialArg 作为初始状态
initialState = ((initialArg: any): S);
}
// 3. 保存初始状态
hook.memoizedState = hook.baseState = initialState;
// 4. 创建更新队列
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer, // 保存用户自定义的 reducer
lastRenderedState: (initialState: any),
};
hook.queue = queue;
// 5. 创建 dispatch 函数并绑定当前 Fiber 和队列
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(null, currentlyRenderingFiber, queue): any));
return [hook.memoizedState, dispatch];
}

核心流程梳理:

useReducer(reducer, initialState, init)

mountReducer

创建 Hook 并加入链表(mountWorkInProgressHook)

计算初始状态(init(initialArg) 或 initialArg)

保存初始状态(memoizedState,baseState)

创建更新队列 queue

创建 dispatch

返回 [state, dispatch]

其中 queue 是该 Hook 的更新队列,结构如下:

// 更新队列
type UpdateQueue<S, A> = {
pending: Update<S, A> | null, // 待处理的更新链表(是个环形链表)
lanes: Lanes, // 队列中包含的车道
dispatch: (A => mixed) | null, // dispatch 函数
lastRenderedReducer: ((S, A) => S) | null, // 上次渲染的 reducer
lastRenderedState: S | null, // 上次渲染的状态
}

另外一个值得关注的点是,dispatch 是一个绑定了 Fiber 和 queue 的函数,引用始终保持稳定,不作为 useEffect 依赖,避免不必要的重新执行。

接下来再看看 dispatch 函数的内部实现:

// dispatch 函数的实现,用于触发 reducer 更新
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
): void {
// 1. 请求更新优先级
const lane = requestUpdateLane(fiber);
// 2. 创建更新对象
const update: Update<S, A> = {
lane,
revertLane: NoLane,
gesture: null,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 3. 判断更新时机并加入队列
// 3.1 Render 阶段更新
if (isRenderPhaseUpdate(fiber)) {
// 加入 render 队列
enqueueRenderPhaseUpdate(queue, update);
}
// 3.2 正常更新
else {
// 将更新添加到并发更新队列中,并返回根节点以触发调度。
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 调度这个更新(让 React 安排时机执行更新)
scheduleUpdateOnFiber(root, fiber, lane);
// 用于将该 useReducer 的更新队列跟 Transition Lane 绑定
// 只有当 requestUpdateLane 请求到的 lane 是「过渡 Lane」(比如 dispatch 被 useTransition 包裹,触发的低优先级更新)时,函数内部才会执行逻辑;
// 如果是高优先级(比如用户输入、手势),该函数直接跳过。
entangleTransitionUpdate(root, queue, lane);
}
}
}

核心流程梳理:

Render 阶段

正常更新

dispatch(action)

dispatchReducerAction

请求优先级(requestUpdateLane)

创建更新对象

判断更新时机

加入 render 队列(enqueueRenderPhaseUpdate)

加入并发更新队列(enqueueConcurrentHookUpdate)

调度渲染(scheduleUpdateOnFiber)

transition 纠缠(entangleTransitionUpdate)

其中 update 是更新对象,结构如下:

// 表示一次状态更新
type Update<S, A> = {
lane: Lane, // 优先级车道
revertLane: Lane, // 回滚车道
action: A, // reducer 的 action
hasEagerState: boolean, // 是否进行了急切计算
eagerState: S | null, // 急切计算的状态
next: Update<S, A>, // 链表指针
gesture: null | ScheduledGesture,
};

Lane 的确定

实现:packages/react-reconciler/src/ReactFiberWorkLoop.js 使用 requestUpdateLane(fiber) 来确定。它会根据以下因素决定更新的优先级:

  • 渲染模式(Legacy vs Concurrent)
  • 调用上下文(渲染阶段 vs 正常阶段)
  • 是否在过渡中(Transition)
  • 当前事件优先级

渲染阶段的更新处理

使用 isRenderPhaseUpdate(fiber) 来判断是否 render 阶段的更新(在 render 中调用 setState 触发的更新),如果是就调用 enqueueRenderPhaseUpdate(queue, update) 进行处理。 enqueueRenderPhaseUpdate 会设置 didScheduleRenderPhaseUpdateDuringThisPassdidScheduleRenderPhaseUpdate 标记,并将 update 对象加入 queue.pending 环形链表,但是不触发调度。

当当前渲染完后,前文提到的 renderWithHooks 函数(函数组件的渲染入口函数),会检测到标志,进入 renderWithHooksAgain 逻辑,进行重新渲染,于是,update 不用经过调度就直接得到了快速的执行(同步)。

很显然,在更新中触发更新,本身很容易造成无限循环。renderWithHooksAgain 的实现中有防止无限循环的机制。渲染过程出错也有清理机制。

最后,虽然这个功能存在,但应该避免使用。正常的状态更新应该在事件处理函数或副作用(Effect)中进行,而不是在渲染过程中。

enqueueRenderPhaseUpdate 是 React 的一个容错机制,React 团队不鼓励这种模式,因为:

  • 容易导致无限循环
  • 破坏渲染的幂等性(同一输入应该产生同一输出)
  • 难以推理:状态更新和渲染逻辑混在一起
  • 性能问题:可能触发多次重新渲染

正常更新

使用 enqueueConcurrentHookUpdate 将更新加入队列并返回根节点。 完成入队后,继续调用 scheduleUpdateOnFiber 进行调度,调用 entangleTransitionUpdate 将更新队列跟 Transition Lane 绑定(如果 lane 是 Transition Lane 的话,否则忽略)。

至此,mount 阶段的 useReducer 的实现就看完了,总体流程都比较清晰。

update 阶段的 useReducer

update 阶段,useReducer 使用 HooksDispatcherOnUpdate 派发 updateReducer

// useReducer 和 useState 的核心入口
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook(); // 复用 Hook 状态和队列
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

useReducer 入口流程

useReducer(reducer, initialState, init)

updateReducer

复用 Hook 状态和队列(updateWorkInProgressHook)

返回 updateReducerImpl 的结果

update 阶段的入口函数很简洁,显示获取(复用)Hook,然后调用 updateReducerImpl

updateReducerImpl 实现了 React 的并发渲染特性,让不同优先级的更新能够正确地交错执行,保证最终状态的一致性。

这个函数非常重要,也比较难,我尽量细致注释其实现:

/**
* updateReducerImpl - useReducer 和 useState 状态更新的核心实现
*
* 职责:
* 1. 合并 pending 队列和 base 队列(处理新到来的更新)
* 2. 按优先级遍历更新队列,跳过低优先级更新
* 3. 计算最终状态并返回
*
* 关键概念:
* - baseQueue: 上次渲染后剩余的未处理更新队列(循环链表)
* - pendingQueue: 本次渲染前新增的更新队列(循环链表)
* - baseState: 已确认的"基准"状态,所有已应用更新计算出的状态
* - memoizedState: 当前渲染使用的状态
* - lane: 更新的优先级标识(位掩码)
* - revertLane: 乐观更新回滚优先级(NoLane 表示非乐观更新)
*
* @param hook - 当前 workInProgress 的 Hook 对象
* @param current - 当前已渲染的 Hook 对象(用于双缓冲比对)
* @param reducer - 状态更新函数
* @returns [新状态, dispatch 函数]
*/
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
// 边界检查
if (queue === null) {
// 如果 queue 为 null,说明 Hook 被条件调用(违反 Hooks 规则)
throw new Error(/*略*/);
}
// 保存当前 reducer 到队列,供 DevTools 和内部调试使用
queue.lastRenderedReducer = reducer;
// baseQueue:上次渲染后剩余的未处理更新(可能因为优先级不足被跳过)
// 这些更新需要保留,以便后续高优先级渲染时 rebase
let baseQueue = hook.baseQueue;
// pendingQueue:本次渲染前新加入的更新(尚未处理)
const pendingQueue = queue.pending;
// 一、合并队列
// 将新增的 pending 队列合并到 base 队列中
if (pendingQueue !== null) {
// 有新更新需要处理,将 pending 队列合并到 base 队列
if (baseQueue !== null) {
// 两个队列都存在,需要合并两个循环链表
// 合并策略:将 pending 队列插入到 base 队列的尾部
// 这样可以保证更新的时间顺序(先 base 的旧更新,后 pending 的新更新)
const baseFirst = baseQueue.next; // base 队列的第一个更新
const pendingFirst = pendingQueue.next; // pending 队列的第一个更新
baseQueue.next = pendingFirst; // base 队列尾部指向 pending 队列头部
pendingQueue.next = baseFirst; // pending 队列尾部指向 base 队列头部
}
// 删减:DEV 环境下的内部不变量检查(确保 workInProgress 队列是 current 的克隆)
// 将合并后的队列赋值给 current 和 baseQueue,并清空 pending
// 这样下次渲染时,这些更新就是"旧的" baseQueue 了
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 二、处理更新队列
const baseState = hook.baseState; // 基准状态:所有已确认更新计算出的状态
// 没有待处理的更新
if (baseQueue === null) {
// 直接使用 baseState
// 注意:memoizedState 和 baseState 可能在 useOptimistic 中不同
// 因为 useOptimistic 每次渲染都接受新的 baseState
hook.memoizedState = baseState;
// 不需要标记更新,因为 baseState 派生自其他响应式值
}
// 有待处理的更新
else {
// 需要遍历队列并计算新状态
const first = baseQueue.next; // 循环链表的第一个节点
let newState = baseState; // 从 baseState 开始计算
// 新的基准状态和队列:
// - newBaseState: 跳过某些更新后,第一个被跳过更新之前的状态
// - newBaseQueue: 被跳过的更新组成的队列(循环链表),供下次渲染使用
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast: Update<S, A> | null = null;
let update = first;
// 用于跟踪是否处理了纠缠的异步 Action(用于 Suspense)
let didReadFromEntangledAsyncAction = false;
// 遍历循环链表,逐个处理更新
do {
// 移除 OffscreenLane 标记位(用于区分隐藏树中的更新)
// OffscreenLane 是一个特殊的 lane,用于标记组件在隐藏状态下的更新
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
// 判断是否应该跳过此更新:
// 1. 如果是隐藏更新,检查 root 的 render lanes
// 2. 否则,检查当前的 render lanes
// 只有当 update.lane 是 renderLanes 的子集时,才处理此更新
let shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
// 删减:Gesture 转换的特殊处理(enableGestureTransition 特性)
// 情况 1: 优先级不足,跳过此更新
if (shouldSkipUpdate) {
// 克隆此更新,加入 newBaseQueue,供下次渲染使用
const clone: Update<S, A> = {
lane: updateLane, // 保留原优先级,下次可能满足
revertLane: update.revertLane, // 乐观更新的回滚优先级
gesture: update.gesture,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
// 第一个被跳过的更新,记录此时的新状态作为 newBaseState
// newBaseState 是"如果从跳过位置重新计算"的起始状态
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState; // 当前状态成为新的基准状态
} else {
// 后续被跳过的更新,追加到队列尾部
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 将此更新的优先级添加到 fiber 的 lanes 中
// 这样下次渲染时,如果 renderLanes 包含这个优先级,就会处理此更新
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane); // 标记跳过的 lanes
}
// 情况 2: 优先级足够,需要处理此更新
else {
// 检查是否是乐观更新(revertLane !== NoLane)
// 乐观更新:临时应用的状态,可能在 transition 完成后回滚
const revertLane = update.revertLane;
// 子情况 2.1: 非乐观更新,直接应用
if (revertLane === NoLane) {
// 但如果之前有跳过的更新,需要将当前更新也加入 newBaseQueue
// 这样下次渲染时可以从 newBaseState 重新计算(rebase)
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// 使用 NoLane(0),因为此更新即将被提交,不需要再被跳过
// 0 是所有位掩码的子集,永远不会被跳过检查所过滤
lane: NoLane,
revertLane: NoLane,
gesture: null,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 检查此更新是否属于待处理的异步 Action
// 如果是,需要抛出 thenable 触发 Suspense,等待 Action 完成
if (updateLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
}
// 子情况 2.2: 乐观更新,需要特殊处理
else {
if (isSubsetOfLanes(renderLanes, revertLane)) {
// 乐观更新:如果"回滚优先级"满足,说明 transition 已完成
// 跳过此更新(假装它不存在),实现回滚效果
update = update.next;
// 检查是否属于待处理的异步 Action
if (revertLane === peekEntangledActionLane()) {
didReadFromEntangledAsyncAction = true;
}
continue; // 跳过此更新,不计算状态
}
else {
// 乐观更新:transition 未完成,需要应用此更新
// 将更新加入 newBaseQueue,以便后续可以回滚或 rebase
const clone: Update<S, A> = {
// 使用 NoLane,因为此更新已被应用,不应再被跳过
lane: NoLane,
// 保留 revertLane,以便知道 transition 何时完成
revertLane: update.revertLane,
gesture: null, // 提交后不再是 gesture 更新
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 将回滚优先级加入 fiber 的 lanes
// 当 transition 完成后,renderLanes 会包含 revertLane,触发回滚
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}
// 应用更新,计算新状态
const action = update.action;
// 省略:DEV 模式下的双重调用检查(shouldDoubleInvokeUserFnsInHooksDEV)
if (update.hasEagerState) {
// 如果有预先计算的状态(eagerState),直接使用
// 这通常是 useState 在 dispatch 时优化计算的结果
newState = ((update.eagerState: any): S);
} else {
// 否则,调用 reducer 计算新状态
newState = reducer(newState, action);
}
}
// 移动到下一个更新
update = update.next;
} while (update !== null && update !== first); // 循环直到回到起点
// 三、完成 newBaseQueue 的构建
if (newBaseQueueLast === null) {
// 所有更新都被处理了,没有跳过的更新
// newBaseState 就是最终状态
newBaseState = newState;
} else {
// 有跳过的更新,需要将链表首尾相连,形成循环链表
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// 四、标记状态变化
if (!is(newState, hook.memoizedState)) {
// 新状态与旧状态不同,标记此 fiber 执行了工作
markWorkInProgressReceivedUpdate();
// 如果处理了纠缠的异步 Action,需要抛出 thenable 触发 Suspense
// 这样可以将更新与同一 Action 的未来更新批处理在一起
if (didReadFromEntangledAsyncAction) {
const entangledActionThenable = peekEntangledActionThenable();
if (entangledActionThenable !== null) {
// 抛出 thenable,React 会捕获并挂起渲染
// 等 Action 完成后恢复渲染
throw entangledActionThenable;
}
}
}
// 保存新状态和队列到 Hook 对象
hook.memoizedState = newState; // 当前渲染使用的状态
hook.baseState = newBaseState; // 下次渲染的基准状态
hook.baseQueue = newBaseQueueLast; // 下次渲染需要处理的队列
// 保存最终状态到队列,供调试使用
queue.lastRenderedState = newState;
}
// 五、清理空队列
if (baseQueue === null) {
// 队列为空,重置 queue.lanes
// queue.lanes 用于 transition 的纠缠(entangling)
queue.lanes = NoLanes;
}
// 返回新状态和 dispatch 函数
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

updateReducerImpluseReduceruseState 处理状态更新的核心实现,负责合并更新队列、处理优先级、计算新状态。其实现比较复杂,我们先梳理下流程。

骨干流程

updateReducerImpl

合并队列

处理更新队列

完成 newBaseQueue 的构建

标记状态变化

清空队列

返回

newState, dispatch

合并队列

第一个环节是,合并队列,将两个循环链表连接成一个。目的是保证更新按时间顺序处理、支持优先级跳过后的 Rebase,避免丢失更新。

流程梳理:

队列合并子流程

queue === null

抛出错误

Hooks 条件调用

设置 lastRenderedReducer

queue.lastRenderedReducer = reducer

pendingQueue !== null

跳过合并

继续使用 baseQueue

baseQueue !== null

环形链表合并

baseQueue.next = pendingFirst

pendingQueue.next = baseFirst

baseQueue = pendingQueue

清空 pendingQueue

queue.pending = null

完成合并

接下来分析下这个过程,首先是两个队列的来源:

let baseQueue = hook.baseQueue; // 基础队列:上次未处理完的更新
const pendingQueue = queue.pending; // 待处理队列:刚加入的新更新

baseQueue(基础队列)

  • 上次渲染时因优先级不足而跳过的更新
  • 这些更新还没有被应用到状态上
  • 会保留到下次渲染时再处理

pendingQueue(待处理队列)

  • 上次渲染完成后,用户新触发的更新
  • 存储在 queue.pending
  • 还没有和之前的更新合并过

注意 baseQueue / pendingQueue 都是环形链表。

我们看看合并是怎么做的:

合并前
baseQueue: ABC → (A)
pendingQueue: XY → (X)
合并后
baseQueue: ABCXY → (A)
^^^^^^
新的更新插入到这里
处理更新

更新队列子流程:

跳过更新

应用更新

普通更新?

乐观更新?

遍历更新队列

初始化变量(newState,newBaseState,newBaseQueue)

取链表头 update

处理 update

检查 update 的 Lane 判断是否跳过(shouldSkipUpdate)

克隆 update

第一次跳过?

克隆的更新保存为 newBaseQueueLast

克隆的更新追加到 newBaseQueueLast

newBaseState 记录跳过前状态

合并 Lane 到当前 Fiber

下一个 update

应用 update

处理普通更新

处理乐观更新

存在跳过节点?

克隆更新到 newBaseQueueLast,设置NoLane

处理更新 action

需要回滚?

克隆更新到 newBaseQueueLast,设置NoLane

直接忽略该更新

update.hasEagerState

使用 eagerState

使用 reducer(newState, action)

遍历完成?(检查下一个update)

遍历结束

我们先看看,逻辑中的一大堆 state 都是什么:

  • **baseState** 只读,本次渲染的起点状态,是上次渲染结束时的 newBaseState

  • **newState**

    1. 从本次渲染的 baseState 开始,每个应用的更新都修改它
    2. 本次渲染结束时写入 memoizedState
  • **memoizedState** 本次渲染的最终状态(渲染给用户看)

  • **newBaseState**

    1. 本次渲染结束时的值就是下次渲染的 baseState
    2. 从空开始,记录首次跳过时的 newState,如果没有跳过,则在本次渲染结束时更新为 newState

为什么要设计这么多 state?主要是为了实现跳过更新rebase 机制。

  • 跳过更新 = 在并发渲染中,低优先级的更新会被暂时跳过,先应用高优先级的更新。此过程跳过点之后的更新队列会被克隆备用。
  • rebase = 在并发渲染中,之前跳过的低优先级更新被应用后,基于之前的跳过点,重新按照顺序应用之前克隆的更新队列。

注意,非跳过节点被克隆时,会设置为 NoLane,这样在下次更新时候,就能被消费,具体看下面的例子。

用一个例子说明整个跳过更新、rebase 的过程:

==================
1 次渲染SyncLane
==================
状态
- baseState = 0
- newState = 0
- memoizedState = 0
- newBaseState = null
- newBaseQueueLast = null
baseQueueSync(+1) → Transition(+10) → Sync(+2) → Transition(+5)
处理过程
1. Sync(+1) → 【应用】:
- newState = 1
- newBaseState = null
- newBaseQueueLast = null
2. Transition(+10) → 【跳过】,注意此处就是跳过点
- newState = 1
- newBaseState = 1
- newBaseQueueLast = Transition(+10) clone // 首次跳过,保存克隆
3. Sync(+2) → 【应用】:
- newState = 3
- newBaseState = 1
- newBaseQueueLast = Transition(+10) cloneSync(+2) NoLane clone // 在跳过点之后,所以也要克隆
4. Transition(+5) → 【跳过】:
- newState = 3
- newBaseState = 1
- newBaseQueueLast = Transition(+10) cloneSync(+2) NoLane cloneTransition(+5) clone // 保存克隆
渲染结束时
- newState = 3
- newBaseState = 1
- newBaseQueueLast = Transition(+10) cloneSync(+2) NoLane cloneTransition(+5) clone
- memoizedState = 3 // 由 newState 写入
==========================
2 次渲染Transition Lane
==========================
状态
- baseState = 1 // 上一轮的 newBaseState
- newState = 1 // 从 baseState 开始
- memoizedState = 3
- newBaseState = null
- newBaseQueueLast = null
baseQueueTransition(+10) cloneSync(+2) NoLane cloneTransition(+5) clone
下次渲染处理过程
1. Transition(+10) clone → 【应用】:
- newState = 11
- newBaseState = null
- newBaseQueueLast = null
2. Sync(+2) NoLane clone → 【应用】,注意 NoLane 0所以总是被应用
- newState = 13
- newBaseState = null
- newBaseQueueLast = null
3. Transition(+5) clone → 【应用】:
- newState = 16
- newBaseState = null
- newBaseQueueLast = null
渲染结束时
- newState = 16
- newBaseState = null
- newBaseQueueLast = null
- memoizedState = 16 // 由 newState 写入

从这个例子,可以清楚看出,这些 state 的设计的作用。

乐观更新

乐观更新通过 update.revertLane 来判断,如果判断是乐观更新,则需要进一步确定其“回滚优先级”是否足够,足够则说明 transition 已完成,需要回滚(跳过此更新)。 否则,就需要克隆该更新到 newBaseQueueLast,然后将回滚优先级加入 Fiber 的 lanes,当 transition 完成后,renderLanes 会包含 revertLane,从而触发回滚。

至此,useReducer 的源码分析完毕。

useState 的实现

理解了以上的 useReducer 之后,useState 就很简单了,它们之间共享了许多代码,下面使用源码来分析实现。

注:源码都做了精简或调整,以便讲清楚核心流程

mount 阶段的 useState

mount 阶段,useState 使用 HooksDispatcherOnMount 这个 Dispatcher,派发的是 mountState,其实现如下:

packages/react-reconciler/src/ReactFiberHooks.js
// 首次渲染:useState 的实现
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 1. 创建 Hook 对象并初始化状态
const hook = mountStateImpl(initialState);
// 2. 获取更新队列
const queue = hook.queue;
// 3. 创建 dispatch 函数(setState),(与 useReducer 中的一样)
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber, // 绑定当前渲染的 Fiber
queue, // 绑定更新队列
): any);
// 4. 将 dispatch 保存到队列中,后续更新使用同一个 dispatch
queue.dispatch = dispatch;
// 5. 返回 [状态值, dispatch 函数]
return [hook.memoizedState, dispatch];
}
// 创建 Hook 并初始化状态
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
// 创建 Hook 对象并加入链表
const hook = mountWorkInProgressHook();
// 计算初始状态(支持函数形式)
if (typeof initialState === 'function') {
// useState(() => expensiveComputation())
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
// 保存初始状态到 memoizedState 和 baseState
hook.memoizedState = hook.baseState = initialState;
// 创建更新队列(与 useReduce 相同)
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null, // 待处理的更新(环形链表)
lanes: NoLanes, // 优先级标记
dispatch: null, // setState 函数(稍后设置)
lastRenderedReducer: basicStateReducer, // 上次使用的 reducer
lastRenderedState: (initialState: any), // 上次渲染的状态
};
hook.queue = queue;
return hook;
}
// setState 的实现,用于触发状态更新
function dispatchSetState<S, A>(
fiber: Fiber, // 当前渲染的 Fiber 节点
queue: UpdateQueue<S, A>, // 更新队列
action: A, // 新的状态或更新函数
): void {
// 1. 请求更新优先级(Lane),这个函数决定了 setState 的"紧急程度":
const lane = requestUpdateLane(fiber);
// 2. 将更新加入队列,并判断是否需要调度渲染
const didScheduleUpdate = dispatchSetStateInternal(
fiber,
queue,
action,
lane,
);
}
// setState 的内部核心逻辑:创建更新并加入队列
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
// 创建更新对象
const update: Update<S, A> = {
lane, // 优先级
revertLane: NoLane, // 回滚优先级(用于 Transition)
gesture: null, // 手势更新(实验性功能)
action, // 状态值或更新函数
hasEagerState: false, // 是否预先计算了状态
eagerState: null, // 预先计算的状态
next: (null: any), // 指向下一个更新(环形链表)
};
// 处理 render 阶段的更新(在 render 中调用 setState 触发的更新)
if (isRenderPhaseUpdate(fiber)) {
// 将更新加入 render 阶段的更新队列
enqueueRenderPhaseUpdate(queue, update);
}
// 处理正常更新(Commit 阶段或事件处理中)
else {
const alternate = fiber.alternate;
// 优化:Eager Bailout(提前退出)===
// 如果当前 Fiber 没有待处理的更新,尝试提前计算新状态
// 如果状态未变化,直接跳过渲染
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// 省略相关代码...
}
// 正常流程:将更新加入队列并调度渲染
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane); // 调度渲染
entangleTransitionUpdate(root, queue, lane); // 绑定 Transition(纠缠)
return true;
}
}
return false;
}
// useState 使用的 reducer:支持函数式更新
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// action 可能是值或函数:setState(1) 或 setState(s => s + 1)
return typeof action === 'function' ? action(state) : action;
}

接下来我们总结下 mount 阶段的代码流程:

useState 流程

函数

非函数

useState(initialState)

mountState

mountStateImpl

创建 Hook 并加入链表(mountWorkInProgressHook)

initialState 类型

调用 initialState()

直接使用 initialState

保存状态(memoizedState,baseState)

创建队列 queue 和 dispatch

返回 [state, setState]

setState 流程

setState(newState)

dispatchSetState

请求优先级(requestUpdateLane)

创建更新对象(dispatchSetStateInternal)

是否 render 阶段更新?

Render 阶段更新

enqueueRenderPhaseUpdate

标记重新渲染标志

renderWithHooksAgain

正常更新(非 render 阶段)

Eager Bailout 检查

计算新状态

eagerState

eagerState === currentState?

跳过渲染

继续处理

加入队列(enqueueConcurrentHookUpdate)

更新环形链表

调度渲染(scheduleUpdateOnFiber)

安排渲染任务

处理 transition(entangleTransitionUpdate)

细节机制
  • dispatch 单例:在 mount 阶段通过 bind 绑定 Fiber 和队列,保存到 queue.dispatch,update 阶段直接复用该 dispatch(引用稳定)
  • Eager Bailout:提前计算新状态,如果未变化则跳过渲染(性能优化)
  • Transition 纠缠:将当前 Transition 更新与其他同优先级的 Transition 更新绑定,确保它们要么一起执行,要么一起跳过,避免状态撕裂
reducer 模式

从代码上看,mount 阶段,useStateuseReducer 使用了相似的设计,也共享了不少逻辑代码。

  • reduceruseState 使用预定义的 basicStateReducer 实现,而 useReducer 则由用户自定义 reducer,更灵活
  • dispatchuseState 使用 dispatchSetStateuseReducer 使用 dispatchReducerAction,核心逻辑也一致
update 阶段的 setState

update 阶段,useState 使用 HooksDispatcherOnUpdate 这个 Dispatcher,派发的是 updateState

其实现如下:

// useState 的更新实现
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}

可以看到,update 阶段的 useState 只是对 useReducer 的简单包裹。

useTransition 的实现

useTransition 是 React 并发模式的核心 Hook 之一,用于标记非紧急的更新,允许 React 优先处理更重要的用户交互。本节通过源码分析其实现原理。

提示:useTransition 的工作原理涉及优先级调度、乐观更新和异步状态管理等机制,建议先理解 useState 的实现后再阅读本节。

// 典型的 useTransition 用法
const [isPending, startTransition] = useTransition();
startTransition(() => {
setSearchQuery(input); // 这个更新会被标记为 transition,可以被打断
});
mount 阶段的 useTransition

mount 阶段,useTransition 使用 HooksDispatcherOnMount 派发 mountTransition

// useTransition 的 Mount 阶段实现
function mountTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
// 1. 使用了前文分析过的 mountStateImpl 来创建状态 Hook:isPending 的初始值为 false
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
// 2. 创建 start 函数并绑定当前 Fiber 和队列
// bind 参数:null, fiber, queue, pendingState(false), finishedState(false)
const start = startTransition.bind(
null,
currentlyRenderingFiber,
stateHook.queue,
true, // pendingState:transition 开始时的状态(isPending = true)
false, // finishedState:transition 结束时的状态(isPending = false)
);
// 3. 将 start 函数保存到 Hook 的 memoizedState
// 这样在 update 阶段可以复用同一个函数引用
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
// 4. 返回 [isPending, start]
return [false, start];
}

这个入口函数中,创建了两个 Hook

stateHook:

通过 mountStateImpl 创建,存储 isPending 状态(booleanThenable)。用于跟踪转换是否处于挂起状态。

hook:

第二个 Hook 通过 mountWorkInProgressHook() 创建。在 memoizedState 中存储 start 函数,该函数通过 bind 绑定了 stateHook.queue 等上下文信息。

这种设计让 useTransition 能够:

  • 返回 isPending 状态用于 UI 显示
  • 返回稳定的 start 函数引用用于触发转换

其中 start 函数,是由 startTransition 绑定部分参数得到的。

startTransition 函数用于标记一个更新为 transition(低优先级更新):

// startTransition 函数的核心实现
function startTransition<S>(
fiber: Fiber, // 当前组件的 Fiber 节点
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>, // 状态更新队列
pendingState: S, // transition 开始时的状态
finishedState: S, // transition 结束时的状态
callback: () => mixed, // 用户传入的回调函数
options?: StartTransitionOptions, // 可选配置
): void {
// 阶段 1:设置更新优先级
const previousPriority = getCurrentUpdatePriority();
// 将当前更新优先级设置为 ContinuousEventPriority(过渡优先级)
// 这使得 transition 更新可以被高优先级更新(如用户输入)打断
setCurrentUpdatePriority(
higherEventPriority(previousPriority, ContinuousEventPriority),
);
// 阶段 2:创建 transition 上下文
const prevTransition = ReactSharedInternals.T;
const currentTransition: Transition = ({}: any);
// 省略:特性开关(view transition、gesture、tracing 等)
// 阶段 3:设置 isPending = true(乐观更新)
ReactSharedInternals.T = currentTransition;
// 立即设置 isPending 为 true,让 UI 显示"加载中"状态
dispatchOptimisticSetState(fiber, false, queue, pendingState);
try {
// 阶段 4:执行用户回调
const returnValue = callback();
// 阶段 5:处理异步 action
// 检查回调是否返回 Thenable(如 async 函数)
if (
returnValue !== null &&
typeof returnValue === 'object' &&
typeof returnValue.then === 'function'
) {
const thenable = ((returnValue: any): Thenable<mixed>);
// 创建一个新的 Thenable,在原始 thenable 完成后解析为 finishedState
const thenableForFinishedState = chainThenableValue(
thenable,
finishedState,
);
// 使用 setState 实现中的 dispatchSetStateInternal 创建更新并加入队列
// 调度状态更新:isPending = thenableForFinishedState
// 这会使得组件 suspend 直到 thenable 完成
dispatchSetStateInternal(
fiber,
queue,
(thenableForFinishedState: any),
requestUpdateLane(fiber),
);
}
// 阶段 6:处理同步 action
else {
// 使用 setState 实现中的 dispatchSetStateInternal 创建更新并加入队列
// 同步完成:直接设置 isPending = false
dispatchSetStateInternal(
fiber,
queue,
finishedState,
requestUpdateLane(fiber),
);
}
}
// 阶段 7:错误处理
catch (error) {
// 创建一个 rejected thenable,让 useTransition 能够重新抛出错误
const rejectedThenable: RejectedThenable<S> = {
then() {},
status: 'rejected',
reason: error,
};
// 使用 setState 实现中的 dispatchSetStateInternal 创建更新并加入队列
dispatchSetStateInternal(
fiber,
queue,
rejectedThenable,
requestUpdateLane(fiber),
);
}
// 阶段 8:恢复上下文和清理
finally {
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}

startTransition

设置更新优先级(ContinuousEventPriority 可被打断)

创建 transition 上下文

发起乐观更新(dispatchOptimisticSetState)

执行用户回调

检查返回值类型是否 Thenable

异步 action

chainThenableValue

dispatchSetStateInternal thenableForFinishedState

同步 action

dispatchSetStateInternal finishedState

恢复优先级

清理 transition 上下文

核心设计:

  • 优先级降级:使用 ContinuousEventPriority 允许被高优先级事件打断
  • 乐观更新:先设置 isPending = false,快速响应用户操作
  • 异步支持:通过 Thenable 机制支持异步操作,完成后自动恢复状态
  • 错误处理:通过 rejected thenable 将错误传递给 useTransition
  • 嵌套转换:支持 transition 嵌套,共享相同的 types 集合

dispatchOptimisticSetState 乐观更新

dispatchOptimisticSetState 用于立即应用乐观更新(设置 isPending = true),在 transition 完成后会被”回退”:

// 乐观更新:立即应用状态,后续可能被回退
function dispatchOptimisticSetState<S, A>(
fiber: Fiber,
throwIfDuringRender: boolean,
queue: UpdateQueue<S, A>,
action: A,
): void {
const transition = requestCurrentTransition();
// 确定更新优先级(省略 gesture 相关逻辑)
const lane = SyncLane;
// 创建更新对象
const update: Update<S, A> = {
lane: lane,
// 乐观更新提交后,使用关联 transition 的 lane 进行 "回退"
revertLane: requestTransitionLane(transition),
gesture: null,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
// 处理 render 阶段更新
if (isRenderPhaseUpdate(fiber)) {
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
}
}
// 调度更新
else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
// 省略:gesture transition 处理
}
}
}

关键在于 update 对象上的 revertLane,从前文的 updateReducerImpl 实现分析中,我们知道有该标志,则会被判断为乐观更新,走对应的处理逻辑。

revertLane 会通过 requestTransitionLane 获取,这个函数的实现中,确保了同一个事件内的所有相同优先级的更新必须被分配到同一个 Lane。这是 React 并发渲染的核心优化策略,确保:

  • 避免同一个事件触发多次渲染
  • 提高批量更新效率
  • 保证状态更新的一致性
update 阶段的 useTransition

update 阶段,useTransition 使用 HooksDispatcherOnUpdate 派发 updateTransition

// useTransition 的 Update 阶段实现
function updateTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
// 1. 获取 isPending 状态
// booleanOrThenable 可能是:
// - boolean:同步 transition,直接返回
// - Thenable<boolean>:异步 transition,需要使用 use() 等待
const [booleanOrThenable] = updateState(false);
// 2. 获取 start 函数(复用 mount 阶段创建的函数)
const hook = updateWorkInProgressHook();
const start = hook.memoizedState;
// 3. 判断 isPending 的值
const isPending =
typeof booleanOrThenable === 'boolean'
? booleanOrThenable // 同步 transition,直接使用 boolean 值
: useThenable(booleanOrThenable); // 异步 transition,使用 use() 等待 Thenable
return [isPending, start];
}

函数内,通过 updateState(update 阶段 useState 的实现)获得 isPending 状态的 Hook,用于取得第一个返回值。 然后,通过 updateWorkInProgressHook 获得第二个 Hook,对应 mount 阶段用来存储 start 函数的 Hook,从该 Hook 中取出 start 函数,作为第二个返回值。

关键机制解读

通过源码分析,我们可以总结出 useTransition 的关键机制:

  • 乐观更新(Optimistic Update) dispatchOptimisticSetState 立即设置 isPending = true,让 UI 快速响应。这个更新使用 SyncLane,会立即提交到屏幕,但设置了 revertLane,后续会被 “回退”。

  • 优先级降级 通过 setCurrentUpdatePriority(ContinuousEventPriority),将 transition 内的更新优先级降低,使得高优先级更新(如用户输入)可以打断 transition。

  • 同步 vs 异步 action

    • 同步 action:回调立即返回,isPending 同步设置为 false
    • 异步 action:回调返回 Thenable,组件会 suspend 直到 Thenable 完成
  • Thenable 处理 使用 chainThenableValue 创建新的 Thenable,将异步 action 的结果转换为 isPending 的最终状态(false)。这允许在 transition 中使用 async/await

  • start 函数稳定性 start 函数在 mount 阶段创建后,引用始终保持稳定(通过 hook.memoizedState 保存),可以作为 useEffect 的依赖而不会触发不必要的重新执行。

  • 嵌套 transition 通过 ReactSharedInternals.T 保存 transition 栈,嵌套的 transition 会继承父级的 types,形成”纠缠”的 transition 组。

  • 错误边界处理 通过创建 rejectedThenable,将同步错误转换为 Thenable,让 useTransition 能够在 useThenable 中重新抛出错误,配合错误边界使用。

至此,完成对 useTransition 的分析。

useEffect 的实现

mount 阶段,useEffect 使用 HooksDispatcherOnMount 这个 Dispatcher,派发的是 mountEffect,其实现如下:

function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}

入口函数很简洁,再看看内部实现:

function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags,
createEffectInstance(),
create,
nextDeps,
);
}

这个函数干了这些事情:

  1. mountWorkInProgressHook(): 创建一个新的 Hook 对象
  2. 处理依赖项: 如果 depsundefined,转为 null
  3. 设置 Fiber 标志: currentlyRenderingFiber.flags |= fiberFlags
  • 告诉 React 这个 Fiber 节点有 effect 需要处理
  • PassiveEffect: 表示是副作用(useEffect
  • PassiveStaticEffect: 静态 effect 优化
  1. 保存 effect: 将 effect 保存到 HookmemoizedState

再看看 pushSimpleEffect 的实现:

function pushSimpleEffect(
tag: HookFlags,
inst: EffectInstance,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): Effect {
const effect: Effect = {
tag,
create,
deps,
inst,
// Circular
next: (null: any),
};
return pushEffectImpl(effect);
}

可以看到关键数据结构 Effect,包含以下字段:

  • tag: effect 类型标志,包括:
    • PassiveEffect: 标记这是一个 passive effect(useEffect),在 layout 之后异步执行
    • PassiveStaticEffect: 静态 effect 优化标志
    • MountPassiveDevEffect: 开发模式下立即执行 effect,帮助发现问题
    • HookPassive: Hook 级别的 passive 标志
    • HookHasEffect: 标记此 effect 需要执行(mount 时总是为 true)
  • create: effect 创建函数
  • deps: 依赖数组
  • inst: effect 实例(包含 destroy 销毁函数)
  • next: 指向下一个 effect(构成环形链表)

该函数内部创建了 Effect 结构。

最后再看看 pushEffectImpl:

function pushEffectImpl(effect: Effect): Effect {
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
}
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
// 第一个 effect
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 后续 effect,插入到环形链表
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}

pushEffectImpl 是 React 将 effect 加入函数组件 effect 链表的核心函数,它维护着一个环形链表结构。

该环形链表的结构定义:

type FunctionComponentUpdateQueue = {
lastEffect: Effect | null, // 指向环形链表的最后一个 effect
events: Array<...> | null, // useEffectEvent 相关
stores: Array<...> | null, // useSyncExternalStore 相关
memoCache: MemoCache | null, // useMemo 缓存
};

所以 mount 阶段 useEffect 整个过程所做的事情都比较简单,就是将函数、依赖以及一些其他信息,打包到 Effect 数据结构中,并使用一个环形链表来维护。

接下来再看看 update 阶段 useEffect 又做了什么。

update 阶段,useEffect 使用 HooksDispatcherOnUpdate 这个 Dispatcher,派发的是 updateEffect,其实现如下:

function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

入口很简单,看内部实现:

function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
// 本次渲染的依赖数组
const nextDeps = deps === undefined ? null : deps;
// 上一次渲染的 effect 对象
const effect: Effect = hook.memoizedState;
// effect 实例(包含 destroy 函数),保持不变
const inst = effect.inst;
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
// currentHook 是 上一次渲染对应的 hook(current 树上的)
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// 依赖未变化,不需要执行 effect
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushSimpleEffect(
hookFlags, // 没有 HookHasEffect 标志!
inst,
create,
nextDeps,
);
return;
}
}
}
// 依赖变化了,标记 effect 需要执行
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushSimpleEffect(
HookHasEffect | hookFlags, // HookHasEffect 标记此 effect 需要执行
inst,
create,
nextDeps,
);
}

updateEffectImpl 实现了依赖项比较机制,决定是否需要重新执行 effect。

依赖项比较逻辑:

updateEffectImpl

currentHook 存在?

标记需要执行 effect(首次渲染后的第二次渲染等情况)

本次渲染的依赖数组存在?

标记需要执行 effect

依赖变化?

跳过 effect 执行(不标记 HookHasEffect)

需要执行 effect(标记 HookHasEffect)

特殊情况

情况 A: currentHook === null,发生在以下场景:

  • 首次渲染后的第二次渲染(但组件从未 commit)
  • 渲染阶段更新触发的重渲染
  • Strict Mode 的双重渲染

这些场景,直接标记需要执行 effect,不比较依赖

情况 B: nextDeps === null,发生在用户代码没有传递依赖数组的情况:

useEffect(() => {}) //(没有传依赖数组)

这种情况也是每次更新都会执行 effect。

依赖数组存在的时候,使用 areHookInputsEqual 函数进行比较,实现:

function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}

可以注意到,使用了 Object.is 的同款逻辑(即 SameValue 算法)进行比较,所以能正确判断 NaN-00 等值。

更多 Hook 实现

待编写

Hooks 只能在组件函数中调用的原因

Hooks 的实现依赖 currentlyRenderingFibercurrentHook 这两个全局变量:

  • currentlyRenderingFiber:指向当前正在渲染的 Fiber 节点
  • currentHook:指向当前正在处理的 Hook

只有在 React 渲染函数组件时,这两个变量才会被正确设置。在普通函数中调用 Hook,这两个变量为 null,会导致错误。

Hooks 只能顶层调用的原因

Hook 是根据执行顺序一个个创建,并以单向链表形式保存在 Fiber 上的,这就意味着,后续更新也只能通过位置来定位到对应的 Hook,所以 Hook 链条必须保持相对位置稳定、数量稳定。

在以上前提下,就不能在条件分支内调用 Hooks,例如:

function HookDemo2() {
const [count, setCount] = useState(0)
if (someCondition) {
useEffect(() => {/**/}, [])
}
const value = useMemo(() => {/**/}, [])
//...
}

这个函数式组件对应的 Fiber 的 memoizedState,就可能出现两种不同的分支:

Fiber.memoizedState → Hook1 (useState) → Hook2 (useEffect) → Hook3 (useMemo) → null
Fiber.memoizedState → Hook1 (useState) → Hook2 (useMemo) → null

这会导致 React 更新渲染时按顺序取 Hook 出现错位,useMemo 可能取到 useEffect 的数据,从而导致状态混乱。

而 React 选择一刀切,只允许顶层调用,就是最简单有效的问题解决方法(做控制流分析非常难,也没必要)。


常见坑点与解决

闭包陷阱

React 开发中常被讨论的闭包陷阱,本身并不是 React 的缺陷导致的问题。只不过 React 在设计上,比较容易让这类问题出现,所以也确实提高了开发者的心智负担。

React 闭包陷阱的本质是:

  1. 组件函数内定义的函数,捕获了它定义时的外部变量(这就是 JavaScript 闭包的正常行为,本身没啥问题)。
  2. 组件函数每次重新运行,都会创建这些外部变量的新版本(这是 React 的函数式风格设计)。
  3. 组件函数重新运行过后,创建了新版变量,因为某种原因,旧版的闭包运行了,由于它使用了定义它时的旧版变量,因此如果不是故意这么设计的,那结果就会出乎意料。

看个简单的例子理解下:

function Counter() {
const [count, setCount] = useState(0)
useEffect(
() => {
// 注意这是个闭包,捕获了外部的 `count` 变量
const closure = () => {
console.log(count) // 始终输出 0
}
// 通过定时器,让这个版本的闭包在未来调用
const timer = setInterval(closure, 1000)
return () => clearInterval(timer)
},
// 依赖为空,effect 只执行一次,
// 这样一来,之前定义的闭包就永远是组件首次执行时的那个版本
// 闭包捕获的也是组件首次执行时的那个版本
[]
)
// 通过 setCount 触发组件重新运行
return <button onClick={() => setCount(count + 1)}>{count}</button>
}

这个例子中,我们在组件渲染后,快速点几下按钮,然后等定时器触发,可以发现,控制台输出的,永远都是 0

通过上面例子代码的注释,结合上面对闭包陷阱本质的揭示,问题就很清晰了。

既然清楚了问题的缘由,那解决起来也就比较简单了,有两种思路。

第一种思路,我们让闭包也变成新版。

就上面的例子而言,我们只需要给 useEffect 的依赖数组,加上准确的依赖就行了。

useEffect(
() => {
const closure = () => {
console.log(count) // 正确输出最新捕获到的 `count`
}
const timer = setInterval(closure, 1000)
return () => clearInterval(timer)
},
// 依赖 `count`,每次组件重新运行,`useEffect` 的逻辑就会重新运行
// 意味着闭包也会被重新创建,捕获到最新版本的 `count`
[count]
)

提醒一下,请妥善处理 useEffect 的清理逻辑。通常简单的反复创建闭包,也不会是多大的性能负担,所以大部分情况,通过妥善配置依赖数组就可以简单消除闭包问题又不引入更重的心智负担了。

第二种思路,我们让旧版闭包能拿到新版变量

这种思路也比较简单,可以利用一个“稳定”(不会随着组件重新渲染反复创建)的对象,内部去持有最新的外部变量值,闭包里面只需要访问这个稳定的对象,就可以随时拿到最新的值了。

React 已经提供了一个很适合的 Hook 来做这个事情了,那就是 useRef,看代码理解:

function Counter() {
const [count, setCount] = useState(0)
// countRef 就是一个稳定的对象,内部引用了一个值
const countRef = useRef(count)
// 组件更新时,也同步更新 countRef,确保引用到最新的变量版本
useEffect(() => {
countRef.current = count
}, [count])
useEffect(() => {
const closure = () => {
console.log(countRef.current) // 正确输出 countRef 引用的最新版变量
}
const timer = setInterval(closure, 1000)
return () => clearInterval(timer)
}, []) // 无需重新运行 useEffect,使用旧版的闭包
return <button onClick={() => setCount(count + 1)}>{count}</button>
}

为了性能优化又或者其他原因,有时候我们不得不面对使用旧版闭包,比如使用 useCallback 加空数组依赖包裹回调等场景。这时候就得从这种思路上着手来解决问题了。

平时使用哪种思路?

从上面两个例子代码来看,使用重新创建闭包的思路,总体是更加简单的,它只是通过少许的性能代价就解决了问题,并不引入更重的心智负担,所以,我个人是推荐大部分情况使用这种思路去解决问题的。

而使用 useRef 这种手工维护变量最新引用的做法,通常应该用在真正需要细致操作的复杂场景中,比如为了性能或者其他原因不得不缓存闭包。

实际开发场景中,组件可能非常复杂,闭包问题可能埋藏得更深,但是理解了原理之后,仔细分析终归能有解决方法的。

最佳实践:使用最新官方标准方案

上面介绍的只是通用的问题解决思路,其实很多时候,我们得找找官方最新提供的工具中,有没有能直接消除问题的做法。

就这个例子而言,如果用的 React 版本支持,完全可以通过使用 useEffectEvent 一步到位解决问题。

function Counter() {
const [count, setCount] = useState(0)
// 使用 `useEffectEvent` 包裹闭包
const tickEvent = useEffectEvent(() => {
console.log(count) // 始终能拿到最新的 count
})
useEffect(
() => {
const timer = setInterval(tickEvent, 1000)
return () => clearInterval(timer)
},
// 无需添加 `tickEvent` 或 `count` 到依赖数组中
[]
)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}

useEffectEvent 的命名已经能看出来了,对于事件处理器(如 onClick)等典型会遭遇闭包问题的场景,就非常适合用该 hook 包裹,直接消除掉闭包问题。

useState 对象和数组的引用未变化

useState 的常见错误是直接修改对象、数组,导致无法正确触发更新。

错误示例

function Form() {
const [form, setForm] = useState({
name: '',
email: '',
age: 0
})
// 错误:直接修改
const handleNameChange = (e) => {
form.name = e.target.value
setForm(form) // 引用未变,React 不会更新
}
return <input value={form.name} onChange={handleNameChange} />
}

修正方式

const handleNameChange = (e) => {
// 创建新对象
setForm({
...form,
name: e.target.value
})
// 或使用函数式更新:
// setForm(prev => ({
// ...prev,
// name: e.target.value
// }))
}

useEffect 依赖项的正确使用

通常情况下,useEffect 必须正确配置依赖项:用到什么,就配上什么,不应缺少或过多配置依赖。

错误做法 1:缺少依赖项

function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetchUser(userId).then(setUser)
}, []) // 缺少 userId 依赖
return <div>{user?.name}</div>
}

该示例中,userId 变化时 effect 不会重新执行,父组件传入新的 userId,但组件仍显示旧数据。正确做法是在依赖数组中加入 userId

useEffect(() => {
fetchUser(userId).then(setUser)
}, [userId]) // 加上依赖

错误做法 2:过多配置依赖

function Counter() {
const [count, setCount] = useState(0)
const [doubled, setDoubled] = useState(0)
useEffect(() => {
setDoubled(count * 2)
}, [count, doubled]) // doubled 不需要加入依赖
return <div>{count} x 2 = {doubled}</div>
}

doubled 是在 effect 内部更新的,不应作为依赖,否则会导致无限循环:effect 执行 → 更新 doubled → 再次触发 effect。

修正方式:移除 doubled 依赖

useEffect(() => {
setDoubled(count * 2)
}, [count]) // 移除 doubled

正确做法总结

除非为特殊目的专门设计,否则请永远诚实声明依赖。effect 内部用到的所有 propsstate 都应加入依赖数组。

性能优化

鉴于最新的 React 性能表现已经相当不错,而性能优化通常又会引入额外的复杂性。因此在绝大部分场景中,都不应使用任何性能优化手段

只有到了不得不做性能优化的时候,才应考虑 React 提供的丰富优化手段。下面我们探讨常见的性能优化方式。

情性初始化

useState 支持惰性初始化函数,避免昂贵的计算在每次渲染时重复执行。

function Component({ userId }) {
// 错误:每次渲染都会调用 expensiveComputation
const [data, setData] = useState(expensiveComputation(userId))
// 正确:只在首次渲染时调用
const [data, setData] = useState(() => expensiveComputation(userId))
return <div>{data}</div>
}

避免不必要的渲染

useCallback

避免不必要的子组件渲染

useCallback 可以与 React.memo 配合使用,避免不必要的子组件渲染。memo 是 React 提供的高阶组件,它会对 props 进行浅比较,只有当 props 发生变化时才重新渲染组件。

function Parent() {
const [count, setCount] = useState(0)
const [name, setName] = useState('')
// 错误:每次 name 变化,handleClick 都会重新创建
// 导致 ChildComponent 不必要的重渲染
const handleClick = () => {
console.log('clicked')
}
// 正确:使用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log('clicked')
}, [])
// 错误:依赖数组包含频繁变化的值
const handleClickBad = useCallback(() => {
console.log('count:', count)
}, [count])
// 正确:使用函数式更新
const handleClickGood = useCallback(() => {
setCount(prev => prev + 1)
}, [])
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
<ChildComponent onClick={handleClickGood} />
</>
)
}
const ChildComponent = memo(({ onClick }) => {
console.log('ChildComponent rendered')
return <button onClick={onClick}>Child</button>
})

作为其他 Hook 的依赖

function SearchComponent({ query }) {
const [results, setResults] = useState([])
// 正确:fetchData 作为 useEffect 的依赖
const fetchData = useCallback(async () => {
const response = await fetch(`/api/search?q=${query}`)
const data = await response.json()
setResults(data)
}, [query]) // query 变化时重新创建 fetchData
useEffect(() => {
fetchData()
}, [fetchData]) // fetchData 变化时重新执行
return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
}
useMemo

useMemo 可用于实现以下性能目的:

1. 缓存昂贵的计算

对于计算密集型操作,useMemo 可以避免在每次渲染时重复计算。注意:只有当计算真正昂贵,且依赖项变化不频繁时,才应该使用 useMemo

function ExpensiveComponent({ items }) {
// 错误:每次渲染都重新计算
// const sorted = items.sort((a, b) => a.value - b.value)
// const filtered = sorted.filter(item => item.active)
// 正确:使用 useMemo 缓存计算结果
// 只有当 items 引用变化时才重新排序和过滤
const sorted = useMemo(
() => [...items].sort((a, b) => a.value - b.value),
[items]
)
const filtered = useMemo(
() => sorted.filter(item => item.active),
[sorted]
)
return <ul>{filtered.map(item => <li key={item.id}>{item.name}</li>)}</ul>
}

2. 防止对象引用变化

function Component({ styleConfig }) {
const [count, setCount] = useState(0)
// 错误:每次渲染都创建新对象
// const style = {
// fontSize: styleConfig.size,
// color: styleConfig.color
// }
// 正确:使用 useMemo 缓存对象
const style = useMemo(
() => ({
fontSize: styleConfig.size,
color: styleConfig.color
}),
[styleConfig.size, styleConfig.color]
)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<div style={style}>Content</div>
</>
)
}

useTransition、useDeferredValue(React 18+)

useTransition 用于将非紧急更新标记为 Transition,避免阻塞用户交互。useTransition 适合用于更新量大、计算密集的场景。

useDeferredValue 用于延迟更新某个值,让该值的更新作为低优先级处理。当某个状态的更新会触发昂贵的渲染,但你又不想阻塞其他更重要的交互时适用。

两者区别是:useTransition 用于包装更新操作,而 useDeferredValue 用于包装某个值本身。

用法详见前面的并发特性章节。


参考


全文完