1. 阅读地图
如果把 React 当成一台机器,它大致由四个层次组成:
初学 React 时,最容易把“组件函数执行”“DOM 更新”“状态变化”混在一起。更准确的理解是:
- JSX 不是 DOM。JSX 会编译成 React element,element 是普通 JS 对象,用来描述你想要的 UI。
- 组件不是实例。函数组件每次渲染就是一次函数调用,组件的状态保存在对应 Fiber 的 Hook 链表里,而不是保存在函数本身。
- 更新不是立刻改 DOM。setState 会创建 update,打上优先级,进入调度,之后 React 在 render 阶段计算新树,在 commit 阶段一次性提交副作用。
- render 阶段可以被打断。现代 React 的并发能力来自 Fiber 可中断、可恢复、可分片的工作单元模型。
- commit 阶段不可被打断。因为它要真实修改宿主环境,必须保证一致性。
2. React 到底是什么
React 通常被称为 UI library,而不是完整 framework。这个叫法有历史原因:React 核心只关心如何把状态映射成 UI,以及如何在状态变化后高效更新 UI。路由、数据请求、构建、服务端渲染、文件约定、接口层通常由 Next.js、Remix、React Router、Vite、TanStack Query 等生态承担。
2.1 React 的三个基本承诺
| 承诺 | 含义 | 带来的架构结果 |
|---|---|---|
| 声明式 | 你描述某个状态下 UI 应该长什么样,而不是手动一步步操作 DOM。 | React 必须保存上一次 UI 描述,并在下一次描述出现时计算差异。 |
| 组件化 | UI 被拆成可组合的组件,组件接收 props,拥有或派生 state。 | React 必须建立一棵和组件嵌套关系对应的工作树,用来保存状态、依赖和副作用。 |
| 跨平台渲染 | React 核心不绑定 DOM,同一个思想可以渲染到浏览器、原生、服务端流。 | React 分成 reconciler 和 renderer:前者算“要做什么”,后者执行“怎么做”。 |
2.2 React 不是模板引擎,也不是虚拟 DOM 库这么简单
“虚拟 DOM”是 React 的一个历史标签,但它并不足以描述现代 React。React 的真正价值在于把 UI 更新抽象成可调度的计算:
- 它能在不同优先级之间切换,比如输入框更新优先于列表过滤结果。
- 它能在渲染过程中暂停、继续或丢弃某次计算,避免长任务阻塞主线程。
- 它能通过 Suspense 把异步资源、加载状态、流式服务端渲染接入同一套 UI 模型。
- 它能通过 Server Components 让部分组件只在服务端执行,减少客户端 JS 和数据瀑布流。
2.3 现代 React 的心智模型
现代 React 更像一个 UI 操作系统:
- Element 是 UI 描述对象,像程序的输入。
- Fiber 是运行时工作单元,像进程控制块,保存组件状态、更新队列、子节点、兄弟节点、优先级、副作用。
- Scheduler 像任务调度器,决定什么时候做哪类更新。
- Reconciler 像 diff 和执行计划生成器,计算树的变化。
- Renderer 像设备驱动,把变更落实到 DOM、Native View 或服务端流。
3. 架构演进
3.1 Stack Reconciler 时代
早期 React 使用递归调用栈进行协调。组件树越深,递归越长。如果某次更新涉及大量节点,React 会一直占用主线程,直到整棵树计算完成。浏览器在这期间无法及时响应输入、动画和绘制,用户感知就是卡顿。
这种架构的问题不是“diff 不够快”,而是“不可调度”:一旦开始递归,就很难暂停、保存进度、切换到更高优先级任务。
3.2 Fiber 时代
Fiber 的核心动机是把一次大递归拆成很多小工作单元。每个 Fiber 节点既代表一个组件或宿主节点,也代表一段可执行、可暂停、可恢复的工作。React 可以在处理一个 Fiber 后检查是否应该让出主线程。
Fiber 不是为了让每次更新都更少,而是为了让更新更可控。它让 React 能做并发渲染、时间切片、优先级调度、Suspense、Transitions 等能力。
3.3 React 18 的关键变化
React 18 把并发渲染能力正式带入稳定版,并引入了新的 root API:createRoot。这不是一个普通 API 替换,它代表应用进入 concurrent-capable root。常见能力包括:
- 自动批处理:更多场景下多个状态更新会合并成一次渲染。
startTransition/useTransition:把非紧急更新标为 transition。useDeferredValue:延迟某个值驱动的低优先级 UI。- Suspense SSR:服务端可以流式发送 HTML,并让客户端分段 hydrate。
useSyncExternalStore:为外部 store 在并发渲染下提供一致读取协议。
3.4 React 19/19.2 的关键变化
React 19 强化了“异步交互”和“服务端组件模型”。官方发布说明中强调 Actions、useActionState、useOptimistic、use、Server Actions、ref 作为普通 prop、<Context> provider、资源预加载和更好的 hydration 错误信息。React 19.2 又加入了 React Performance Tracks、cacheSignal、局部预渲染相关 API、SSR Suspense reveal batching 等能力。
4. React 包与分层
真实项目里我们经常安装 react 和 react-dom,但源码和架构层次远不止这两个包。
| 层 | 典型包/模块 | 职责 |
|---|---|---|
| 公共 API | react |
暴露 createElement、hooks、context、memo、lazy、Suspense 等用户 API。 |
| 协调器 | react-reconciler |
构建和更新 Fiber 树,计算更新、副作用、优先级,是 React 的核心。 |
| 调度器 | scheduler |
根据优先级安排任务,配合浏览器主线程空闲时间和消息通道执行 work loop。 |
| DOM 渲染器 | react-dom |
把 Fiber 的 host 操作落实为 DOM 创建、属性更新、事件绑定、hydration、server rendering。 |
| 服务端渲染 | react-dom/server、react-dom/static |
生成 HTML 字符串或流,支持 Suspense streaming、prerender、resume 等。 |
| 开发工具 | React DevTools | 读取 Fiber 树、组件名、props/state、渲染原因、Profiler 数据。 |
4.1 Reconciler 和 Renderer 的边界
React 核心并不知道 DOM 节点怎么创建。它只知道“这里需要放一个 host component,类型是 div,props 是 {...}”。真正执行 document.createElement、appendChild、removeChild 的是 DOM renderer。
这也是 React 能支持 React Native 的原因:同一套 reconciler 可以把 host operation 委托给不同 renderer。DOM renderer 创建真实 DOM,Native renderer 创建原生视图,服务端 renderer 输出 HTML/流式 payload。
4.2 为什么说 React Element 是跨平台描述
const element = <button className="primary">Save</button>;
// 编译后大致类似:
const element = jsx("button", {
className: "primary",
children: "Save"
});
这个对象没有真实 DOM 方法,也没有事件监听器实例。它只是描述:类型是什么、props 是什么、key 是什么、ref 是什么。React 后续会根据 element 创建或更新 Fiber,再由 renderer 决定具体平台操作。
5. 实际项目里 React 怎么运行
5.1 最小 CSR 项目链路
以 Vite + React SPA 为例,一次页面启动通常经历:
- 浏览器加载
index.html。 index.html引入构建后的 JS bundle。- bundle 执行,导入
react、react-dom/client和你的App。 createRoot(document.getElementById('root'))创建 React root。root.render(<App />)触发初始更新。- React 创建 FiberRoot 和 HostRoot Fiber,进入 render 阶段构建 work-in-progress tree。
- React 进入 commit 阶段,把 DOM 节点插入容器。
- 浏览器绘制页面,之后事件、状态更新继续驱动后续渲染。
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);
5.2 真实工程的构建运行链路
React 本身不负责模块打包、CSS 处理、路由分包、图片压缩、TypeScript 转换。它只在运行时接管 root 容器内的 UI 更新。框架和构建工具负责把你的模块变成浏览器可执行的资源。
5.3 CSR、SSR、SSG、RSC 的启动差异
| 模式 | 首屏 HTML | 客户端 React 做什么 | 适用场景 |
|---|---|---|---|
| CSR | 通常只有 root 空容器和 script | 下载 JS 后从零创建 DOM | 后台系统、强交互应用、SEO 压力小的应用 |
| SSR | 服务端即时生成完整或部分 HTML | hydrate 现有 HTML,绑定事件并恢复交互 | 内容页、SEO、首屏体验敏感场景 |
| SSG | 构建时生成静态 HTML | hydrate 或局部交互 | 文档、营销页、博客、变化不频繁内容 |
| RSC | 服务端组件输出可被客户端合并的 payload | 只 hydrate client components,合并 server payload | 数据密集、希望减少客户端 JS 的全栈 React 应用 |
5.4 一次点击到 UI 更新的完整路径
6. React 核心工作循环
React 更新可以粗略分为三段:触发更新、render 阶段、commit 阶段。官方文档也用 trigger、render、commit 来解释这个流程。
6.1 Trigger:触发更新
触发更新的来源包括:
- 首次
root.render(element)。 setState或 Hook dispatcher。- 父组件重新渲染导致子组件被访问。
- Context value 变化。
- 外部 store 通知。
- Suspense promise resolve。
- 服务端 payload 到达并合并。
6.2 Render:计算下一棵树
render 阶段不是浏览器绘制,而是 React 调用组件函数,计算新的 React element,并把它们和当前 Fiber 树进行协调。这个阶段会产生 work-in-progress tree,也会收集需要在 commit 阶段执行的副作用标记。
render 阶段应该是纯的。组件函数不应该直接改 DOM、不应该发请求、不应该修改外部变量来影响世界。原因是 render 可能被打断、重试、丢弃、执行多次。开发环境 StrictMode 故意额外调用某些逻辑,就是为了暴露不纯的渲染逻辑。
6.3 Commit:提交副作用
commit 阶段会把 render 阶段算出的变化应用到宿主环境。对 DOM 来说,就是插入、删除、更新节点,设置属性,调用 ref,运行 layout effects,之后安排 passive effects。
commit 不可中断。React 不能把 DOM 改到一半就暂停,否则用户可能看到不一致 UI。
6.4 伪代码版工作循环
function scheduleUpdateOnFiber(fiber, lane) {
const root = markUpdateFromFiberToRoot(fiber, lane);
ensureRootIsScheduled(root);
}
function performConcurrentWorkOnRoot(root) {
renderRootConcurrent(root);
if (workInProgressRootExitStatus === RootCompleted) {
const finishedWork = root.current.alternate;
commitRoot(root, finishedWork);
}
}
function renderRootConcurrent(root) {
prepareFreshStack(root);
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(fiber) {
const next = beginWork(fiber);
fiber.memoizedProps = fiber.pendingProps;
if (next === null) {
completeUnitOfWork(fiber);
} else {
workInProgress = next;
}
}
实际源码远比这复杂,但面试时能讲出这条骨架已经很有含金量。
7. 核心数据结构总览
如果面试官问“React 内部有哪些重要数据结构”,不要只说 Virtual DOM。更完整的回答应该覆盖 element、Fiber、root、update、update queue、lane、hook、effect、context dependency 和 Suspense boundary。下面这张表可以当成速记。
| 数据结构 | 保存什么 | 在哪个流程中关键 | 面试关键词 |
|---|---|---|---|
| ReactElement | type、key、ref、props。 |
JSX 编译后、组件 render 返回值、协调输入。 | 普通对象、UI 描述、不可变快照。 |
| Fiber | 组件运行时节点,保存 props/state、父子兄弟指针、alternate、flags、lanes。 | render work loop、协调、提交。 | 工作单元、双缓存、可中断。 |
| FiberRoot | root 容器、current tree、pending lanes、finishedWork、调度回调、缓存。 | createRoot、调度入口、commitRoot。 | 根管理对象,不等同 HostRoot Fiber。 |
| Update | 一次状态变化,包含 lane、payload、callback、next。 | setState、dispatch、root.render。 | 更新不是立即赋值,而是入队。 |
| UpdateQueue | 一组待处理 update,常见为环形链表或带 baseQueue 的结构。 | 计算新 state、跳过低优先级 update、批处理。 | 队列、函数式 updater、baseState。 |
| Lane | 用 bitmask 表示的更新优先级集合。 | 调度、选择 next lanes、bailout、并发渲染。 | 优先级、合并、跳过、重试。 |
| Hook | 函数组件 Hook 链表节点,保存 memoizedState、queue、next。 | useState、useReducer、useEffect、useMemo 等。 | 顺序匹配、不能条件调用。 |
| Effect | effect create、destroy、deps、tag。 | commit 阶段 layout/passive effect 执行。 | 副作用、依赖数组、cleanup。 |
| Context Dependency | 组件读取过的 context 及其版本/值依赖。 | context value 更新、消费者重新渲染。 | 跨层依赖、更新扩散。 |
| Suspense Boundary | fallback、primary tree、retry lanes、pending promise 相关状态。 | Suspense、streaming SSR、RSC、异步重试。 | 捕获 promise、显示 fallback、resolve 后重试。 |
7.1 ReactElement:最容易被误解的数据
ReactElement 常被叫作“虚拟 DOM”,但这个说法容易让人误以为它是 DOM 的完整模拟。实际上它更像一条轻量 UI 描述记录。
const element = {
$$typeof: Symbol.for("react.element"),
type: "button",
key: "save",
ref: null,
props: {
className: "primary",
children: "Save"
}
};
element 不保存上一次状态,也不保存更新队列。状态和调度信息在 Fiber 上。组件每次 render 返回的是新 element,React 用它作为协调输入。
7.2 Update 与 UpdateQueue:状态更新如何排队
Update 可以理解为一张“状态变更单”。它不一定立刻执行,而是进入队列,等待 React 根据 lane 和调度时机处理。
type Update = {
lane: Lane,
payload: any, // 新值、updater function,或 root.render 的 element
callback: Function | null,
next: Update | null
};
在并发渲染中,一个 update 可能因为优先级不够被跳过。React 会保留 baseState 和 baseQueue,确保之后处理低优先级更新时能从正确基准继续计算。这就是为什么 Lane 和 UpdateQueue 必须一起理解。
7.3 Effect 数据结构:不是所有副作用都一样
Hook effect 会保存 create、destroy、deps 和 tag。tag 会区分 layout effect、passive effect、是否需要重新执行等。render 阶段只记录 effect,commit 阶段才执行。
type Effect = {
tag: HookFlags,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<unknown> | null,
next: Effect
};
7.4 数据结构之间怎么连起来
8. Fiber 架构
7.1 Fiber 是什么
Fiber 可以从三个角度理解:
- 数据结构:它是一个 JS 对象,保存节点类型、props、state、子节点、兄弟节点、父节点、副作用、优先级等。
- 工作单元:React 每次处理一个 Fiber,处理完可以决定继续、暂停或切换优先级。
- 组件实例的运行时承载:函数组件没有 class instance,它的 hooks、更新队列、依赖关系都挂在 Fiber 上。
7.2 Fiber 树的指针结构
Fiber 树不是用数组保存 children,而是用链表式指针:
关键字段:
| 字段 | 作用 |
|---|---|
tag |
Fiber 类型,例如 FunctionComponent、ClassComponent、HostComponent、HostRoot、SuspenseComponent。 |
type |
组件函数、class、DOM 标签名等。 |
key |
协调列表时识别节点身份。 |
stateNode |
关联的实例。HostComponent 对应 DOM 节点,ClassComponent 对应 class 实例,HostRoot 对应 FiberRoot。 |
return |
父 Fiber。 |
child |
第一个子 Fiber。 |
sibling |
下一个兄弟 Fiber。 |
alternate |
双缓存中另一棵树的对应 Fiber。 |
pendingProps |
本次渲染接收到的新 props。 |
memoizedProps |
上次完成渲染后保存的 props。 |
memoizedState |
组件状态。函数组件中通常是 Hook 链表头。 |
updateQueue |
待处理更新队列,class state、host root、effects 等都会用到。 |
flags / subtreeFlags |
本节点或子树需要提交的副作用标记。 |
lanes / childLanes |
本节点或子树待处理更新的优先级集合。 |
7.3 双缓存:current tree 与 work-in-progress tree
React 内部通常同时维护两棵 Fiber 树:
- current tree:当前屏幕已经提交的 UI 对应的 Fiber 树。
- work-in-progress tree:正在计算中的下一棵树。
两棵树上的对应节点通过 alternate 相互指向。更新完成后,React 把 root.current 指向 finishedWork,work-in-progress 变成新的 current。
双缓存让 React 可以在不破坏当前屏幕的情况下计算下一版 UI。如果并发渲染中途被丢弃,用户仍然看到旧的 current tree。
7.4 FiberRoot 和 HostRoot Fiber
这两个概念很容易混:
- FiberRoot 是整个 React root 的管理对象,保存容器、pending lanes、finishedWork、callbackNode、缓存、hydration 信息等。
- HostRoot Fiber 是 Fiber 树的根节点,它的
stateNode指向 FiberRoot。
你调用 createRoot(container) 时,React 会创建 FiberRoot 和 HostRoot Fiber。之后 root.render(<App />) 本质上是在 HostRoot 上入队一个 update,payload 是你的 element。
9. 协调算法
8.1 协调要解决什么问题
每次组件返回新的 element 树,React 需要判断哪些节点可以复用,哪些节点要新建,哪些节点要删除,哪些 props 要更新。这个过程叫 reconciliation。协调的输出不是直接 DOM 操作,而是新的 Fiber 树和 effect flags。
8.2 O(n) diff 的两个假设
通用树 diff 的复杂度很高。React 使用启发式策略,把复杂度压到接近 O(n):
- 不同类型的元素会产生不同树。比如
<div>变成<section>,通常整棵子树重建。 - 开发者可以通过
key告诉 React 哪些子节点在不同渲染之间是同一个身份。
8.3 单节点协调
对于一个旧 Fiber 和一个新 element:
- 如果 key 相同、type 相同:复用旧 Fiber,更新 props。
- 如果 key 不同或 type 不同:旧 Fiber 删除,新建 Fiber。
- 如果是文本节点:比较文本内容,必要时标记更新。
// type 相同,Fiber 复用,DOM 节点也倾向复用
<Button color="red" />
<Button color="blue" />
// type 不同,组件身份改变,状态重置
<Button />
<LinkButton />
8.4 列表协调与 key
列表 diff 的重点是识别“同一个业务项”。key 不传时 React 使用位置作为身份,这在插入、删除、排序时容易导致状态错位。
// 不推荐:列表会变动时,index key 容易导致状态错位
items.map((item, index) => <Todo key={index} todo={item} />)
// 推荐:使用稳定业务 id
items.map(item => <Todo key={item.id} todo={item} />)
面试里常见问题:“为什么不能用 index 作为 key?”更完整的回答是:
8.5 状态保留与重置
React 官方文档强调:state 和 JSX 标签本身无关,而是和组件在 UI 树中的位置相关。只要同一个组件类型在同一个父节点下的同一个身份位置继续出现,React 就倾向保留状态。改变 key 可以强制重置子树。
// contact 改变,但 Chat 在同一位置,状态会保留
<Chat contact={to} />
// key 改变,React 认为是新身份,Chat 及其子树重建
<Chat key={to.id} contact={to} />
这个原则能解释很多现象:为什么条件渲染切换组件会丢状态,为什么把组件定义嵌套在另一个组件内部会导致状态重置,为什么表单有时需要 key 来清空。
8.6 beginWork 和 completeWork
render 阶段对每个 Fiber 大致分两步:
- beginWork:处理当前 Fiber,根据 props/state/context 计算子元素,并协调出子 Fiber。
- completeWork:在子树完成后回到当前 Fiber,创建或更新 host 实例、冒泡 flags 和 lanes。
8.7 bailout:跳过不必要工作
如果一个 Fiber 的 props、state、context 和 lanes 都没有需要处理的变化,React 可以跳过它的子树。这叫 bailout。常见触发方式包括:
React.memo判断 props 没变。- 同一次 render 中某个子树没有对应 lane。
- context 没变化。
- 组件返回相同引用的 element 并不等于必然跳过,但稳定引用有助于 memo 体系。
10. 调度、优先级与 Lane
9.1 为什么需要调度
浏览器主线程同时负责 JS 执行、样式计算、布局、绘制、事件响应。如果 React 一次更新占用主线程太久,用户输入就会延迟。调度的目标是:紧急更新尽快完成,非紧急更新可以延后、打断、重试。
9.2 典型优先级
| 更新类型 | 例子 | 倾向 |
|---|---|---|
| 同步/离散事件 | 点击按钮、输入框按键 | 用户马上期待反馈,优先级高。 |
| 连续事件 | 鼠标移动、滚动、拖拽 | 频繁发生,需要避免阻塞。 |
| 默认更新 | 普通 setState | 常规优先级。 |
| Transition | 搜索结果列表、tab 内容切换、复杂筛选 | 可中断,优先保证当前交互流畅。 |
| Idle | 预渲染、低价值后台工作 | 空闲时再做。 |
9.3 Lane 模型
React 早期使用 expiration time 表示优先级,后来改成 Lane 模型。Lane 可以理解为位掩码表示的一组更新车道。一次更新会被分配到某个 lane,root 上保存 pending lanes,调度时选择优先级最高的一批 lanes 来渲染。
Lane 的好处:
- 可以表达多个更新同时存在。
- 可以合并相近优先级的更新。
- 可以在一棵树上标记子树是否包含某类待处理更新。
- 可以让被打断的 render 和后续更高优先级更新协作。
9.4 startTransition 到底做了什么
const [isPending, startTransition] = useTransition();
function onInput(e) {
const next = e.target.value;
setText(next); // 紧急:输入框立刻更新
startTransition(() => {
setQuery(next); // 非紧急:昂贵列表可以慢一点
});
}
startTransition 不会让代码“异步执行”,而是把其中触发的 React 更新标记为 transition lane。这样当更紧急的输入继续发生时,React 可以暂停或丢弃旧的 transition 渲染,优先保持输入响应。
9.5 自动批处理
批处理是指多个 state update 合并到一次 render。React 18 后,Promise、setTimeout、原生事件等更多场景也会自动批处理。
setCount(c => c + 1);
setFlag(f => !f);
// 通常只触发一次渲染,而不是两次
如果确实需要同步刷新 DOM,可以使用 flushSync,但它会降低并发调度空间,应该谨慎。
11. 提交阶段与 Effects
10.1 flags 是什么
render 阶段不会直接改 DOM,而是在 Fiber 上标记副作用。常见 flags 包括:
Placement:需要插入节点。Update:需要更新属性、文本或执行某些更新。ChildDeletion:需要删除子节点。Ref:需要 attach/detach ref。Passive:需要执行 passive effect,也就是useEffect。Layout:需要执行 layout effect,也就是useLayoutEffect。
10.2 commit 三个主要子阶段
| 阶段 | 做什么 | 对应 API/场景 |
|---|---|---|
| before mutation | DOM 变化前读取快照。 | class 的 getSnapshotBeforeUpdate。 |
| mutation | 真实执行 DOM 插入、删除、属性更新、文本更新、ref detach。 | host operations。 |
| layout | DOM 已更新但浏览器还未必绘制,执行 layout effects 和 ref attach。 | useLayoutEffect、componentDidMount、componentDidUpdate。 |
useEffect 的 passive effects 通常在 commit 后异步执行。它适合订阅、日志、网络请求、非布局相关副作用。useLayoutEffect 会在浏览器绘制前同步执行,适合读取布局并同步调整,但会阻塞绘制,不能滥用。
10.3 effect cleanup 顺序
当依赖变化或组件卸载时,React 会先执行上一次 effect 的 cleanup,再执行新的 effect。这个模型让订阅和资源释放可以跟组件生命周期对齐。
useEffect(() => {
const unsubscribe = store.subscribe(forceUpdate);
return () => unsubscribe();
}, [store]);
10.4 StrictMode 为什么会让 effect 执行两次
开发环境中,StrictMode 会模拟 mount → cleanup → mount,以帮助你发现 effect 没有正确清理的问题。生产环境不会这样重复执行。面试时不要简单说“React bug”或“严格模式渲染两次”,要说它是开发期检查机制。
12. Hooks 与状态
11.1 Hooks 为什么不能写在条件里
函数组件的 Hooks 状态保存在 Fiber 的 memoizedState 链表中。React 按调用顺序把每个 Hook 和链表节点对应起来。条件调用会破坏顺序,导致后续 Hook 读到错误状态。
function Component({ visible }) {
const [a, setA] = useState(0);
if (visible) {
// 错误:visible 变化后 Hook 顺序改变
const [b, setB] = useState(0);
}
const [c, setC] = useState(0);
}
11.2 Hook 数据结构
简化后的 Hook 节点类似:
type Hook = {
memoizedState: any, // 当前状态或 effect 信息
baseState: any, // 跳过低优先级更新时的基准状态
baseQueue: Update | null, // 尚未处理完的基础队列
queue: UpdateQueue | null,
next: Hook | null
};
函数组件每次 render 时,React 设置当前 dispatcher。mount 时 dispatcher 创建 Hook 节点,update 时 dispatcher 按顺序复用旧 Hook 节点并计算新状态。
11.3 useState 的更新队列
setState 并不是直接改变量,而是创建 update 放进 queue。update 可能是值,也可能是 updater function。
setCount(count + 1); // 捕获当前 render 的 count
setCount(c => c + 1); // 基于队列中的前一个状态计算,更适合连续更新
为什么连续三次 setCount(count + 1) 只加一?因为同一次 render 里 count 是固定快照。为什么三次 setCount(c => c + 1) 能加三?因为 updater function 会按队列顺序逐个应用。
11.4 useReducer
useReducer 和 useState 底层很接近,都依赖 Hook update queue。区别是 reducer 把状态转移逻辑集中到一个函数中,更适合复杂状态机。
function reducer(state, action) {
switch (action.type) {
case "add":
return { ...state, items: [...state.items, action.item] };
case "clear":
return { ...state, items: [] };
default:
return state;
}
}
11.5 useMemo 与 useCallback
useMemo 缓存计算结果,useCallback 缓存函数引用。它们不是性能银弹,真正价值通常在两个场景:
- 计算本身昂贵,依赖没变时避免重复计算。
- 配合
React.memo、依赖数组、context value 等需要稳定引用的地方。
过度使用会增加代码复杂度和依赖维护成本。面试里可以说:优化应该先定位瓶颈,再用 memoization 缩小渲染范围或计算成本。
11.6 useRef
useRef 返回稳定对象,ref.current 改变不会触发渲染。它适合保存 DOM 引用、计时器 id、最新回调、可变但不参与渲染的数据。
11.7 React 19 的 ref as prop
React 19 支持函数组件把 ref 当作普通 prop 接收,未来 forwardRef 会逐步弱化。旧代码仍常见 forwardRef,面试或项目迁移时需要知道两种写法。
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
13. 数据流怎么串起来
12.1 React 的默认数据流
React 默认是单向数据流:
单向数据流的优点是可追踪。UI 是 state 的函数,事件是改变 state 的入口。
12.2 props、state、derived state
| 数据类型 | 谁拥有 | 什么时候用 |
|---|---|---|
| props | 父组件 | 子组件只消费,不直接修改。 |
| state | 当前组件 | 组件需要记住随交互变化的数据。 |
| derived state | 可从 props/state 计算 | 优先在 render 中计算,不要重复存储。 |
| server state | 服务端/缓存层 | 远程数据、缓存、同步、失效重取,通常交给 Query 库或框架 loader。 |
| external store | React 外部 | Zustand、Redux、浏览器 API、协同状态等。 |
12.3 不要滥用 useEffect 派生数据
// 不推荐:fullName 可以从 props 直接算,不需要 state + effect
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
// 推荐
const fullName = firstName + " " + lastName;
Effect 是用来同步外部系统的,不是 render 数据流的默认工具。很多 React 项目变复杂,是因为把普通计算、事件逻辑、请求状态、DOM 同步都塞进 useEffect,导致数据流绕路。
12.4 表单数据流
受控组件把输入值放在 React state 中:
function Form() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={e => setName(e.target.value)}
/>
);
}
非受控组件把值留在 DOM 中,通过 ref 或表单提交读取。复杂表单库通常会混合使用非受控输入和订阅机制来减少每次输入导致的 React rerender。
12.5 React 19 Actions 数据流
Actions 让异步交互,尤其是表单提交,可以进入 React 的 pending、error、optimistic UI 模型。常见链路:
const [error, submitAction, isPending] = useActionState(
async (previousError, formData) => {
const name = formData.get("name");
return await updateName(name);
},
null
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Save</button>
{error && <p>{error}</p>}
</form>
);
12.6 项目级数据流建议
- 局部 UI 状态放组件内,比如弹窗开关、当前 tab、输入草稿。
- 跨多个兄弟组件的状态提升到最近公共父组件。
- 主题、登录用户、语言等低频全局数据用 Context。
- 远程数据用框架 loader、RSC、TanStack Query、SWR 等,不要手写一堆 useEffect 管缓存。
- 高频外部状态用专门 store,并通过
useSyncExternalStore或库封装保证并发一致性。
14. 事件系统
13.1 为什么 React 有自己的事件系统
React DOM 的事件系统提供跨浏览器一致性、事件委托、优先级分配和批处理入口。现代 React 通常把事件监听绑定在 root 容器附近,而不是给每个 DOM 节点都绑定真实监听器。
13.2 事件到更新优先级
事件系统不只是调用你的 handler。它还会根据事件类型设置当前更新优先级。例如 click、keydown 等离散事件通常对应更高优先级;mousemove、scroll 这种连续事件则需要更平滑处理。
13.3 SyntheticEvent 现在还需要 persist 吗
旧版 React 有事件池,异步访问事件对象时需要 event.persist()。现代 React 已经不再使用那种事件池机制,通常不需要 persist。面试时可以提到这是老版本知识点。
15. Context 与外部 Store
14.1 Context 适合什么
Context 解决的是“跨层传递 props”的问题,适合低频变化、范围明确的依赖,例如主题、国际化、当前用户、权限上下文、表单上下文。
Context 不等于状态管理库。Context value 一变化,消费它的组件都可能重新渲染。高频更新的大型状态直接塞 Context,容易造成性能问题。
14.2 React 19 的 Context provider 写法
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext value="dark">
<Page />
</ThemeContext>
);
}
旧写法 <ThemeContext.Provider value="dark"> 仍然常见。
14.3 useSyncExternalStore
外部 store 最大的问题是并发渲染下可能出现 tearing:同一次渲染中不同组件读到不一致快照。useSyncExternalStore 提供了标准订阅协议:
const value = useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot
);
状态库可以基于它让 React 在并发模式下安全读取外部数据。
16. Suspense 与异步
15.1 Suspense 解决什么
Suspense 的目标是让“某部分 UI 暂时还没准备好”成为 React 树的一部分。它可以用于代码分割、服务端流式渲染、RSC payload、支持 Suspense 的数据读取等。
<Suspense fallback={<Spinner />}>
<Profile />
</Suspense>
15.2 throw Promise 心智模型
一个支持 Suspense 的读取函数,如果数据没准备好,可以抛出 promise。React 捕获到 promise 后,会找到最近的 Suspense boundary,显示 fallback,并在 promise resolve 后重试渲染。
15.3 lazy 与代码分割
const SettingsPage = lazy(() => import("./SettingsPage"));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<SettingsPage />
</Suspense>
);
}
lazy 本质上把动态 import 的 promise 接入 Suspense。模块没加载完时,boundary 显示 fallback。
15.4 use API
React 19 的 use API 可以在组件中读取 promise 或 context 等资源。它和普通 Hooks 不完全一样,可以在条件中使用,但仍然要遵守 React 对渲染纯度和资源稳定性的要求。实际项目里是否能用,还取决于框架、数据层和运行环境支持。
15.5 Suspense 和 Error Boundary 的关系
- Suspense 捕获 promise,表示“还没准备好”。
- Error Boundary 捕获 error,表示“出错了”。
真实应用里常把两者组合:加载时显示 skeleton,失败时显示错误恢复 UI。
17. SSR、Hydration、Server Components
16.1 SSR 的核心链路
16.2 Hydration 是什么
Hydration 不是重新创建 DOM,而是在已有服务端 HTML 上建立 React Fiber 与 DOM 的对应关系,并绑定事件、运行必要逻辑。如果服务端输出和客户端首次渲染不一致,就会出现 hydration mismatch。React 19 改善了 hydration 错误报告,会给出更清晰的 diff 和原因提示。
16.3 常见 hydration mismatch 原因
- 服务端和客户端条件分支不同,例如 render 中使用
typeof window改变内容。 - 使用
Date.now()、Math.random()直接参与首屏渲染。 - 服务端和客户端 locale/timezone 格式化结果不同。
- 外部数据没有把同一份快照传给客户端。
- 无效 HTML 嵌套被浏览器自动修正。
16.4 Streaming SSR
传统 SSR 要等整棵树渲染完再发 HTML。Streaming SSR 可以先发 shell,再随着 Suspense boundary 完成逐步发送内容。这样首字节和首屏可更快,慢数据不会阻塞整页。
16.5 React Server Components
Server Components 让组件在服务端执行,并把结果作为一种可合并的组件 payload 发给客户端。它的核心价值:
- 减少客户端 JS:Server Component 本身不打进客户端 bundle。
- 直接访问服务端资源:数据库、文件系统、私有 API 可以留在服务端。
- 减少数据瀑布:数据读取靠近组件,服务端可以统一调度。
- 保留组件模型:服务端和客户端组件可以组合,但有边界限制。
16.6 Server Component 与 Client Component 边界
| 能力 | Server Component | Client Component |
|---|---|---|
| 运行位置 | 服务端/构建时/框架运行环境 | 浏览器,也可能先在服务端 SSR |
| useState/useEffect | 不能使用 | 可以使用 |
| 访问数据库 | 可以,取决于运行环境 | 不能直接访问私有服务端资源 |
| 事件处理 | 不能直接写浏览器事件 handler | 可以 |
| bundle 影响 | 不进入客户端 JS | 进入客户端 JS |
注意:"use client" 是 Client Component 边界声明;"use server" 主要用于 Server Actions,并不是“标记 Server Component”的指令。React 官方也特别提示过这个误解。
16.7 Server Actions
Server Actions 允许 Client Component 调用在服务端执行的 async function。框架会把服务端函数变成客户端可引用的 reference。客户端调用时,React/框架发送请求到服务端执行,并把结果返回。
16.8 Partial Pre-rendering 与 resume
React 19.2 提到的 Partial Pre-rendering 思路是:先预渲染静态 shell,把动态部分 postpone,之后在请求时 resume 继续生成流。这类能力离普通业务开发比较远,但面试高阶题可以知道它说明 React 的服务端渲染也在向“可中断、可恢复、可分块”的方向演进。
18. 性能优化
17.1 先区分三类性能问题
| 类型 | 症状 | 解决方向 |
|---|---|---|
| 渲染太多 | 父组件一变,大量子组件跟着 render。 | 拆组件、局部状态、memo、稳定 props、context 分层。 |
| 渲染太慢 | 一次 render 里计算昂贵或列表巨大。 | useMemo、虚拟列表、Web Worker、分页、降低同步计算。 |
| 提交太重 | DOM 节点太多、布局抖动、layout effect 阻塞。 | 减少 DOM、避免同步布局读写、CSS 优化、分块显示。 |
17.2 React.memo 的适用条件
React.memo 会在 props 浅比较相等时跳过函数组件重新渲染。适合:
- 组件渲染成本明显。
- props 大多稳定。
- 父组件频繁更新但子组件实际不受影响。
不适合盲目给所有组件套 memo。如果每次都传新对象、新函数,memo 会失效,还增加比较成本。
17.3 Context 性能
// 不推荐:每次 App render 都创建新对象,消费者都可能更新
<AuthContext value={{ user, logout }}>
{children}
</AuthContext>
// 改进:稳定 value
const authValue = useMemo(() => ({ user, logout }), [user, logout]);
<AuthContext value={authValue}>{children}</AuthContext>
更进一步,可以拆分 Context:读用户信息的组件不应该因为 theme 改变而 rerender,读权限的组件不应该因为弹窗状态改变而 rerender。
17.4 列表性能
大列表优先考虑虚拟滚动。React diff 再快,也不应该让浏览器一次管理几万个 DOM 节点。
- 使用稳定 key。
- 分页、无限滚动或虚拟列表。
- 避免列表 item 内创建大量闭包和对象,必要时 memo item。
- 避免在 render 中对超大数组做昂贵 filter/sort,可提前索引或 memo。
17.5 Profiler 和 React Performance Tracks
React DevTools Profiler 可以查看组件为什么渲染、渲染耗时、提交耗时。React 19.2 的 Performance Tracks 则把 Scheduler 和 Components 信息放进浏览器性能面板,能看到不同优先级任务、transition、render、effect 等轨迹。高阶性能分析会越来越依赖这些工具,而不是凭感觉加 memo。
17.6 常见反模式
- 用 effect 同步本可直接计算的派生 state。
- 把所有状态放全局 store,导致更新范围扩大。
- Context value 每次 render 都是新对象。
- 把 key 写成随机数,导致每次都重建子树。
- 在 render 中执行副作用或修改外部可变对象。
- 把 transition 当 debounce 用。transition 解决优先级,不等于减少触发次数。
- 用 useMemo/useCallback 掩盖组件边界不合理。
19. React Compiler
19.1 React Compiler 解决什么
React Compiler 的目标是自动优化 React 应用中的 memoization,减少手写 React.memo、useMemo、useCallback 的需要。官方文档的说法是:编译器可以自动处理 memoization,让你用更接近普通 React 的写法表达 UI。
它不是传统意义上把 JSX 转成 JS 的编译器。Babel/SWC 处理语法转换,而 React Compiler 更像一个理解 React 语义和数据流的优化器。它会分析组件和 Hook 是否纯、依赖是否可追踪,然后插入更细粒度的缓存逻辑。
19.2 它和 React.memo 有什么不同
| 能力 | React.memo | React Compiler |
|---|---|---|
| 使用方式 | 手动包组件。 | 构建时自动分析和优化。 |
| 粒度 | 组件级 props 浅比较。 | 可以更细粒度地复用 JSX、计算结果和中间值。 |
| 依赖 | 依赖开发者保持 props 引用稳定。 | 依赖代码符合 React 纯渲染和数据流规则。 |
| 风险 | 过度使用导致代码噪音和比较成本。 | 不符合规则的代码可能被跳过优化,需要 lint 和诊断。 |
19.3 Compiler 对面试回答的影响
过去面试常问“为什么要 useCallback”“是不是每个函数都要包 useCallback”。更现代的回答应该是:
19.4 Compiler 不会替你解决的问题
- 不会自动修复不纯 render,例如 render 中修改外部对象。
- 不会让巨大 DOM 列表变小,虚拟列表仍然需要。
- 不会替你设计正确的状态归属和数据流。
- 不会消除服务端数据请求、网络延迟和缓存失效问题。
- 不会把 transition、Suspense、RSC 这些架构能力自动接进业务。
19.5 面试时怎么说
可以这样概括:React 的运行时仍然是 Fiber、调度、协调、提交这套模型;React Compiler 是构建时优化层,帮助运行时少做不必要的重复渲染和计算。它改变的是“手动 memoization 的必要性”,不是 React 核心工作循环本身。
20. 源码阅读路线
React 源码很大,不建议从入口一头扎进去。更好的路线是按“用户 API 到内部流程”逐层读。
18.1 推荐路线
- JSX 到 element:理解
jsx/createElement产物。 - createRoot:理解 FiberRoot、HostRoot Fiber 的创建。
- root.render:理解 HostRoot update 如何入队。
- scheduleUpdateOnFiber:理解从 Fiber 到 root 的 lane 标记和任务安排。
- performConcurrentWorkOnRoot:理解 render work loop。
- beginWork/completeWork:理解协调和 Fiber 生成。
- commitRoot:理解 DOM mutation、layout/passive effects。
- Hooks dispatcher:理解 mount/update Hook 链表与队列。
- Suspense:理解 promise 捕获、boundary、retry lane。
18.2 关键源码文件概念
| 模块方向 | 你要关注的内容 |
|---|---|
| ReactElement | element 对象结构、key/ref 处理、JSX runtime。 |
| ReactFiber | Fiber 节点创建、alternate、work-in-progress。 |
| ReactFiberWorkLoop | 调度入口、renderRoot、work loop、commitRoot。 |
| ReactFiberBeginWork | 不同 tag 的 beginWork 逻辑、bailout、reconcileChildren。 |
| ReactChildFiber | 单节点和列表协调、key diff、placement。 |
| ReactFiberCompleteWork | host instance 创建、props 更新准备、flags 冒泡。 |
| ReactFiberHooks | Hook 链表、dispatcher、update queue、effect 链。 |
| ReactFiberLane | lane 位运算、优先级选择、合并与清理。 |
18.3 源码阅读时的三个锚点
- 永远问:现在是在 render 阶段还是 commit 阶段?
- 永远问:当前处理的是 current tree 还是 work-in-progress tree?
- 永远问:这个更新属于哪个 lane,是否可能被跳过、打断或合并?
21. 面试高频问答
19.1 React Fiber 是什么?为什么要引入 Fiber?
19.2 React 的 render 和 commit 有什么区别?
19.3 setState 后发生了什么?
19.4 为什么函数组件每次执行还能保留 state?
19.5 为什么 Hooks 不能放在条件语句里?
19.6 key 的作用是什么?
19.7 React.memo、useMemo、useCallback 有什么区别?
19.8 useEffect 和 useLayoutEffect 区别?
19.9 React 18/19 的并发渲染是不是多线程?
19.10 startTransition 解决什么问题?
19.11 Suspense 原理是什么?
19.12 SSR 和 Hydration 是什么关系?
19.13 Server Components 和 SSR 有什么区别?
19.14 Context 为什么可能导致性能问题?
19.15 React 如何保证外部 store 在并发渲染下的一致性?
19.16 为什么不要在 render 中做副作用?
19.17 React 中状态更新是同步还是异步?
19.18 为什么组件 state 是快照?
19.19 class 组件和函数组件生命周期如何对应?
19.20 如何排查 React 性能问题?
22. 速记清单
一句话版
- React 是声明式 UI 库,核心是把状态变化调度成 Fiber 树更新,并由 renderer 提交到宿主环境。
- JSX 编译成 element,element 生成 Fiber,Fiber 保存运行时状态和工作进度。
- 更新从 setState 进入 updateQueue,被分配 lane,然后调度 root。
- render 阶段可中断,负责计算;commit 阶段不可中断,负责提交。
- Hooks 状态在 Fiber.memoizedState 链表中,靠调用顺序匹配。
- key 决定同一父节点下的身份,影响复用和状态保留。
- Suspense 捕获 promise,Error Boundary 捕获 error。
- SSR 输出 HTML,hydration 恢复交互,RSC 减少客户端 JS 并把服务端数据逻辑留在服务端。
- 性能优化先测量,再缩小更新范围,最后使用 memo、虚拟列表和 transition 等工具。
面试回答模板
遇到 React 原理题,可以按这个顺序回答:
- 先说用户层现象:调用了什么 API,表现是什么。
- 再说内部数据结构:element、Fiber、Hook、update、lane、effect。
- 再说流程:触发更新 → 调度 → render → commit。
- 最后说边界和取舍:可中断/不可中断、优先级、性能影响、项目实践。
项目实践判断题
- 输入卡顿但结果列表复杂:用 transition、useDeferredValue、虚拟列表。
- 组件无故重渲染:查父组件 state、props 引用、Context value、memo 是否失效。
- 表单切换用户后草稿没清空:检查 key 或状态归属。
- SSR hydration 报错:检查随机数、时间、locale、window 分支、服务端数据快照。
- effect 无限循环:检查依赖数组里是否有每次 render 都新建的对象/函数,或者 effect 内是否无条件 setState。
- 全局状态更新导致整页动:拆分 store selector、Context、状态归属。
23. 参考资料
这份资料结合 React 官方文档和发布说明整理,重点参考:
- React Docs: Render and Commit
- React Docs: Preserving and Resetting State
- React Blog: React 19
- React Blog: React 19.2
- React API Reference
- React Docs: React Compiler
最后更新:2026-05-28。React 生态更新很快,面试准备时建议关注当前项目所用版本。React 18、19 和 19.2 的能力边界不完全相同,回答时最好带上版本上下文。