Chapter 05
原理与前沿:为什么这样设计,又往哪走
前四章把“怎么用”讲透了。这一章先回答“为什么是这样设计”——用三张备选方案表呈现 React 放弃了什么;再讲它正在往哪走:React 19、编译器、服务端组件。前者锁住理解,后者给出截至 2026 年的真实地形。
本章你将建立的 schema
- 三个核心设计的取舍:虚拟 DOM vs 信号(signals)、Hook vs class、不可变 vs 可变
- React 的分层架构:Reconciler 与 Renderer 分离意味着什么
- 前沿现状(带日期):React 19、React Compiler、Server Components 各自改变了什么
5.1架构:Reconciler 与 Renderer 分离
前四章讲的全部——重跑组件、协调、状态、effect——发生在 React 的协调层(Reconciler)。它和把结果落到具体平台的渲染层(Renderer)是分开的:同一套协调逻辑,react-dom 落到浏览器 DOM,react-native 落到原生视图。
5.2为什么用虚拟 DOM,而不是信号
React 选择“state 变就重跑整个组件、diff 虚拟树、最小提交”,换取一个显式、可预测的心智模型。
这个选择的代价正是第 2 章那个默认:父组件重渲染会重跑整棵子树,其中一部分是“算了新描述、diff 后发现没变”的无用功。Solid、Svelte、Vue 走的“信号 / 细粒度响应式”路线没有这笔开销——但要在读值处建立依赖追踪,心智更隐式。React 用第 5.5 节的编译器来抵消这笔代价,而不是改变模型。
| 方案 | 优势 | 为什么 React 没选它 |
|---|---|---|
| 命令式直接操作 DOM | 无抽象开销,改哪动哪 | 要手动保持 DOM 与状态同步,规模一大就失控(第 1 章的痛点) |
| 信号 / 细粒度响应式(Solid、Svelte、Vue 3) | 精确追踪依赖,只更新真正变的节点,无需 diff 整棵树 | 需在读值处建立依赖追踪(编译或包装),心智更隐式;React 偏向“重跑函数”的显式模型 |
| 虚拟 DOM + 协调 | 心智简单:state 变就重跑组件、声明式、无需手动追踪依赖 | 选中 |
5.3为什么是 Hook,而不是 class
函数组件 + Hook 让逻辑按“关注点”聚合、可抽成自定义 Hook 复用,代价是接受第 4 章那条“调用顺序”规则。
| 方案 | 优势 | 为什么被取代 |
|---|---|---|
| class 组件 + 生命周期 | 有实例 this 存状态,曾是标准 | this 绑定易错;同一关注点被切到 didMount/didUpdate/willUnmount 三处;复用靠 HOC / render props,嵌套层层叠加 |
| Mixins(更早) | 能复用逻辑 | 命名冲突、隐式依赖,早已废弃 |
| 函数组件 + Hook | 逻辑按关注点聚合、抽成自定义 Hook 复用、没有 this | 选中 |
5.4为什么坚持不可变
| 方案 | 优势 | 为什么 React 没选它 |
|---|---|---|
| 可变 + 脏检查(Angular 1 风格) | 直接改对象,写法自然 | 要遍历比对找出哪变了,性能随规模下降;变化时机不明确 |
| 可变 + 手动通知(observable / KVO) | 精确,改谁通知谁 | 样板多、容易漏发通知,错漏难查 |
| 不可变 + 引用比较 | Object.is 一次比较就知道变没变;并发下可安全复用旧子树 | 选中 |
代价在第 3 章已经领教:更新嵌套结构要层层展开,啰嗦,于是有 Immer 这类库帮忙。收益是整套优化的地基——下一节的编译器,正是建立在“引用没变就一定没变”这个约定上。
5.5前沿一:React 19 与编译器(截至 2026-06)
React 19(2024-12 稳定)把异步与资源读取纳入核心;React Compiler 1.0(2025-10 GA)让“手动记忆化”基本退场。
React 19:少写样板
React 19 于 2024-12 稳定,几个改变心智的点:Actions 配合 useActionState / useFormStatus / useOptimistic,把表单提交的 pending、错误、乐观更新做成内建;use() 可在渲染中读取 Promise(配合 Suspense)或 Context;ref 成了普通 prop,forwardRef 不再需要。
// React 18 及以前:转发 ref 必须包一层 forwardRef
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
// React 19:ref 就是一个普通 prop(forwardRef 已弃用)
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
React Compiler:手动记忆化退场
React Compiler 1.0 于 2025-10 GA。它在编译期分析组件,自动插入等价于 useMemo / useCallback / React.memo 的缓存,跳过未变部分的重渲染——甚至能在 early-return 之后做记忆化,超出手写能力。结果:新代码基本不再手写这三个 API,它们留作逃生舱。
// 手动记忆化时代:到处包 useMemo / useCallback
const filtered = useMemo(() => items.filter(fn), [items]);
const onClick = useCallback(() => doThing(id), [id]);
// React Compiler(2025-10 GA):写朴素代码,编译器自动插入等价缓存
const filtered = items.filter(fn);
const onClick = () => doThing(id);
编译器优化的是“跳过没必要的重算”,不改变 state → UI 的语义。第 2 章“父重渲染默认重渲染子组件”仍是心智基准;编译器只是把那笔“无用功”自动消除。理解 re-render(前四章)依旧必要——否则你看不懂编译器在替你做什么,也判断不了它什么时候帮不上忙。配套的 lint 已并入 eslint-plugin-react-hooks v6(独立的 eslint-plugin-react-compiler 不再单独安装)。
5.6前沿二:Server Components(截至 2026-06)
组件默认在服务端运行(可直读数据、不进浏览器 bundle);用 "use client" 标记需要交互的组件,它们才下发到浏览器。
“组件是 props 的纯函数”在这里结出果实:纯的、无 state 的展示组件天然适合在服务端运行——它只是把数据映射成描述。带 state / effect / 事件的组件(前四章的交互核心)才需要客户端。"use client" 就是这条边界的声明。前四章的全部心智,在客户端边界之内原样成立。
// 默认在服务端运行:可直接读数据,不进浏览器 bundle
async function ProductPage({ id }) {
const product = await db.products.get(id); // 服务端直读
return <ProductView product={product} />;
}
// 需要交互的组件:用 "use client" 标记,下发到浏览器
"use client";
function AddToCart({ id }) {
const [count, setCount] = useState(1); // state 只能在客户端
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
"use client" 划定——之上放“纯、无 state”的组件(第 1 章那种 f),之下才放 state / effect / 事件。截至 2026-06,RSC 已从 Next.js 扩散到 React Router v7 等框架;2025-12 曾有一则 RSC 安全公告,生产环境记得 pin 并更新版本。5.7跨概念综合:把取舍用起来
一个具体抉择,串起本章与前四章:一个“商品详情 + 加入购物车”的页面,该怎么切分?商品信息的读取与展示——纯函数、无 state(§1.2)——放服务端组件,省下 bundle 与一次客户端请求(§5.6);“加入购物车”的按钮带 count state 与点击事件(§3、§1.4),必须 "use client"。是否要手动 memo 购物车列表?在开了 React Compiler 的项目里不必(§5.5),但你仍要能判断“这次重渲染是否真的多余”——而这个判断,靠的正是第 2 章的 render/commit 区分。新工具没有让旧心智过时,是让它更省力。
§本章 self-check
先合上教程,把答案写下来,再展开对照。
- 用一句话说出 React 选“虚拟 DOM + 协调”而非“信号”所换来的东西,以及为此付出的代价。
- Hook 相对 class 真正解决的问题是什么?它要求你接受哪条规则作为交换?
- React Compiler(2025-10 GA)让哪三个 API 基本不必再手写?它改变了 state → UI 的语义吗?
- (设计题)一个“显示文章正文 + 底部点赞按钮”的页面,用 RSC 该如何切分服务端 / 客户端?依据是什么?
答案(先做完再展开)
- 换来的是显式、可预测的心智(state 变就重跑组件、无需手动追踪依赖);代价是默认重跑整棵子树、可能产生 diff 后无变化的“无用功”render。
- 真正解决的是“逻辑复用与关注点聚合”:把散在多个生命周期方法里的同一关注点收进一个 effect,并可抽成自定义 Hook 复用。交换条件是接受“Hook 必须按固定顺序、无条件调用”的规则(§4.1)。
useMemo/useCallback/React.memo。不改变语义——它只自动跳过没必要的重算,state → UI 的关系不变,所以前四章的心智依然成立。- 正文是纯展示、无 state,放服务端组件(直读数据、不进 bundle);点赞按钮带本地 state 与点击事件,标
"use client"放客户端。依据是“有没有 state / effect / 事件”——有交互才需要客户端。
开了编译器,还需要理解 re-render 吗?
团队给项目开启了 React Compiler,有人说“以后不用关心重渲染了”。给出一个具体场景,说明即使有编译器,不理解前四章仍会写出 bug 或查不出问题。
提示(卡住再展开)
编译器消除的是“多余的重算”,不消除“语义错误”。例如:用下标当 key 导致状态串行(§2.3)——这是正确性 bug,编译器不碰;effect 漏写依赖读到过期快照(§4.3)——编译器不替你补语义;把该显示的值放进 ref 导致界面不更新(§4.2)——编译器无能为力。编译器优化的前提是你的代码语义本就正确;判断“这次重渲染是不是真的多余、是不是有副作用泄漏”,仍然要靠前四章的模型。工具加速正确的代码,不修复错误的心智。