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 落到原生视图。

APP 描述 CORE RENDER HOST 你的组件(render 返回元素) React 元素树(描述) Reconciler · Fiber diff · 可中断 · 前四章都在这层 Renderer:react-dom / react-native 宿主平台:浏览器 DOM
图 5.1React 的分层。注意:Reconciler(Fiber)与 Renderer 是分开的——这就是同一份组件代码能渲染到 DOM 也能渲染到原生的原因。也正因为 Reconciler“可中断”,并发渲染(边算边可放弃)才得以实现。

5.2为什么用虚拟 DOM,而不是信号

React 选择“state 变就重跑整个组件、diff 虚拟树、最小提交”,换取一个显式、可预测的心智模型。

设计的代价(诚实地说)

这个选择的代价正是第 2 章那个默认:父组件重渲染会重跑整棵子树,其中一部分是“算了新描述、diff 后发现没变”的无用功。Solid、Svelte、Vue 走的“信号 / 细粒度响应式”路线没有这笔开销——但要在读值处建立依赖追踪,心智更隐式。React 用第 5.5 节的编译器来抵消这笔代价,而不是改变模型。

表 5.1 · UI 更新机制的三条路线
方案优势为什么 React 没选它
命令式直接操作 DOM无抽象开销,改哪动哪要手动保持 DOM 与状态同步,规模一大就失控(第 1 章的痛点)
信号 / 细粒度响应式(Solid、Svelte、Vue 3)精确追踪依赖,只更新真正变的节点,无需 diff 整棵树需在读值处建立依赖追踪(编译或包装),心智更隐式;React 偏向“重跑函数”的显式模型
虚拟 DOM + 协调心智简单:state 变就重跑组件、声明式、无需手动追踪依赖选中

5.3为什么是 Hook,而不是 class

函数组件 + Hook 让逻辑按“关注点”聚合、可抽成自定义 Hook 复用,代价是接受第 4 章那条“调用顺序”规则。

表 5.2 · 组件与逻辑复用的演进
方案优势为什么被取代
class 组件 + 生命周期有实例 this 存状态,曾是标准this 绑定易错;同一关注点被切到 didMount/didUpdate/willUnmount 三处;复用靠 HOC / render props,嵌套层层叠加
Mixins(更早)能复用逻辑命名冲突、隐式依赖,早已废弃
函数组件 + Hook逻辑按关注点聚合、抽成自定义 Hook 复用、没有 this选中
class · 按生命周期切割 componentDidMount 订阅 + 日志 componentDidUpdate 订阅(重订) + 日志 componentWillUnmount 订阅(退订) Hook · 按关注点聚合 useEffect(订阅) connect + cleanup 收在一处 useEffect(日志) 同一个“订阅”关注点(朱红):左边被切成 3 块,右边收进 1 块 代价:换来的是必须遵守 Hook 调用顺序(§4.1)
图 5.2同一个“订阅”关注点(朱红字)在两种模型里的分布。注意:class 按“生命周期时刻”组织代码,于是一个关注点被迫散在三个方法里;Hook 按“关注点”组织,订阅连同它的清理收进同一个 effect——这就是 Hook 真正解决的问题,不是少打几个字。

5.4为什么坚持不可变

表 5.3 · 变化检测的三种做法
方案优势为什么 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 不再需要。

ref-as-prop.jsx JSX
// 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,它们留作逃生舱。

compiler-memo.jsx JSX
// 手动记忆化时代:到处包 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" 标记需要交互的组件,它们才下发到浏览器。

把第 1 章延伸到服务端

“组件是 props 的纯函数”在这里结出果实:纯的、无 state 的展示组件天然适合在服务端运行——它只是把数据映射成描述。带 state / effect / 事件的组件(前四章的交互核心)才需要客户端。"use client" 就是这条边界的声明。前四章的全部心智,在客户端边界之内原样成立。

server-client-boundary.jsx JSX
// 默认在服务端运行:可直接读数据,不进浏览器 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>;
}
服务端 Server Component 直读数据 · 不进 bundle 纯展示组件 无 state / effect "use client" 边界 客户端(浏览器) Client Component state / effect / 事件 前四章的交互核心在这里 输出
图 5.3服务端 / 客户端边界。注意:边界由 "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

先合上教程,把答案写下来,再展开对照。

  1. 用一句话说出 React 选“虚拟 DOM + 协调”而非“信号”所换来的东西,以及为此付出的代价。
  2. Hook 相对 class 真正解决的问题是什么?它要求你接受哪条规则作为交换?
  3. React Compiler(2025-10 GA)让哪三个 API 基本不必再手写?它改变了 state → UI 的语义吗?
  4. (设计题)一个“显示文章正文 + 底部点赞按钮”的页面,用 RSC 该如何切分服务端 / 客户端?依据是什么?
答案(先做完再展开)
  1. 换来的是显式、可预测的心智(state 变就重跑组件、无需手动追踪依赖);代价是默认重跑整棵子树、可能产生 diff 后无变化的“无用功”render。
  2. 真正解决的是“逻辑复用与关注点聚合”:把散在多个生命周期方法里的同一关注点收进一个 effect,并可抽成自定义 Hook 复用。交换条件是接受“Hook 必须按固定顺序、无条件调用”的规则(§4.1)。
  3. useMemo / useCallback / React.memo。不改变语义——它只自动跳过没必要的重算,state → UI 的关系不变,所以前四章的心智依然成立。
  4. 正文是纯展示、无 state,放服务端组件(直读数据、不进 bundle);点赞按钮带本地 state 与点击事件,标 "use client" 放客户端。依据是“有没有 state / effect / 事件”——有交互才需要客户端。
进阶挑战 · 刚好够不着

开了编译器,还需要理解 re-render 吗?

团队给项目开启了 React Compiler,有人说“以后不用关心重渲染了”。给出一个具体场景,说明即使有编译器,不理解前四章仍会写出 bug 或查不出问题。

提示(卡住再展开)

编译器消除的是“多余的重算”,不消除“语义错误”。例如:用下标当 key 导致状态串行(§2.3)——这是正确性 bug,编译器不碰;effect 漏写依赖读到过期快照(§4.3)——编译器不替你补语义;把该显示的值放进 ref 导致界面不更新(§4.2)——编译器无能为力。编译器优化的前提是你的代码语义本就正确;判断“这次重渲染是不是真的多余、是不是有副作用泄漏”,仍然要靠前四章的模型。工具加速正确的代码,不修复错误的心智。