React 是什么
有必要学透,但 JSX 语法不必背——AI 生成的组件你看得懂、改得动就行。真正要理解的是组件模型、Hooks 的心智模型,以及「哪些该进 state、副作用为什么必须放进 useEffect」这类边界;这些底层原理 AI 替代不了,否则它堆出的组件越乱你越查不出 bug。学到能审查 AI 写的组件、判断状态管理是否合理即可,不必花时间背诵 API 细节,用的时候让 AI 生成并人工核对。
如果你写过 jQuery,或者用原生 API 手动拼过一个稍微复杂点的页面,一定经历过这样的场景:数据变了,你要记得去改标题;列表多了一项,你要记得插入节点;用户点了删除,你要记得同时更新数组、移除元素、刷新计数器。只要漏掉一处,界面就和数据对不上了。
React 给出的答案只有一句话:别去改界面,去改数据;界面由数据算出来。
1. 命令式 UI 的困境
1.1 一个计数器的两种写法
先看原生写法。需求很简单:一个数字,两个按钮,还有一行「当前是奇数/偶数」的提示。
let count = 0;
const valueEl = document.querySelector("#value");
const parityEl = document.querySelector("#parity");
const decBtn = document.querySelector("#dec");
function render() {
valueEl.textContent = String(count);
parityEl.textContent = count % 2 === 0 ? "偶数" : "奇数";
decBtn.disabled = count === 0;
}
document.querySelector("#inc").addEventListener("click", () => {
count += 1;
render();
});
decBtn.addEventListener("click", () => {
count -= 1;
render();
});
render();这段代码本身没什么问题,但请注意它的结构:每次改完数据,你必须手动调用一次同步函数。现在假设需求增加了——加一个「重置」按钮、加一个历史记录列表、加一个动画。每加一处,你都要问自己:这里改完之后,还有哪些地方需要跟着变?
这类 bug 有个共同特征:它不是逻辑错误,而是遗漏。你写的每一行代码都对,只是少写了一行。
1.2 状态与视图的同步成本
问题的本质在于,命令式代码里存在两份「真相」:
| 真相 | 存在于 | 谁负责更新 |
|---|---|---|
| 数据状态 | JS 变量 | 业务代码 |
| 界面状态 | DOM 树 | 手写的同步代码 |
两份真相必须靠人力保持一致,而人力是不可靠的。随着交互路径增多,需要维护的同步关系呈组合爆炸增长:3 个状态、5 个界面区域,理论上就有 15 条同步关系要照顾。
React 的做法是消灭第二份真相。DOM 不再是你维护的对象,而是一个由状态推导出来的结果。
2. 声明式 UI
2.1 UI 是状态的函数
React 的心智模型可以浓缩成一个公式:
UI = f(state)你写的组件就是这个 f。它接收当前状态,返回「界面应该长什么样」的描述。你永远不写「把这个节点的文本改成 X」,只写「当状态是 X 时,界面应该是这样」。
同样的计数器,用 React 写:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p id="value">{count}</p>
<p>{count % 2 === 0 ? "偶数" : "奇数"}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)} disabled={count === 0}>
-1
</button>
</div>
);
}注意这里完全没有更新界面的代码。没有 textContent,没有 disabled = true,没有 render()。你只描述了「界面和 count 的关系」,剩下的交给 React。
一个简单的自测:当你想写「点击后把某个元素隐藏」时,如果第一反应是「找到那个元素,改它的 style」,说明还在命令式思维里。React 的写法是「加一个 visible 状态,让那个元素在 visible 为 false 时不被渲染出来」。
2.2 声明式的代价与收益
声明式不是免费的。它要求你:
- 把所有会影响界面的数据都提升为「状态」,不能藏在闭包或 DOM 里
- 接受「整个组件函数会被反复执行」这个前提,因此函数体内不能有副作用
- 学会用状态表达「界面处于哪一步」,而不是用一连串命令描述「怎么走到这一步」
换来的收益是:
- 新增交互路径不需要新增同步代码。无论用户从哪条路走到 count = 3,界面都一定是 count = 3 该有的样子
- 代码可读性。看一眼组件就知道界面的全部可能形态
- 可测试性。给定状态得到输出,是纯函数式的验证
3. 虚拟 DOM 到底解决了什么
3.1 一个常见的误解
网上流传最广的说法是「虚拟 DOM 比直接操作 DOM 快」。这句话是错的。
手写的、精准的 DOM 操作永远是最快的——因为它只改必须改的那一个属性,没有任何额外开销。React 要先执行组件函数、生成描述对象、和上一次的对比、再算出补丁、最后才落到真实 DOM 上。这个过程只会更慢。
虚拟 DOM 的真正价值是:它让你能写出声明式代码,同时性能不至于崩掉。
3.2 没有虚拟 DOM 的声明式会怎样
假设 React 老老实实执行「UI = f(state)」——状态一变,就把整棵 DOM 全部重建:
container.innerHTML = renderToHTML(state);这样做功能上完全正确,但有三个致命问题:
- 性能灾难。重建 DOM 是昂贵操作,输入框里的每一次按键都要重建整页
- 状态丢失。输入框的光标位置、滚动条位置、正在播放的视频,全都会被重置
- 动画中断。CSS 过渡依赖节点的连续性,节点被替换动画就断了
虚拟 DOM 是在「全量重建」和「手动精准更新」之间的折中:在轻量的 JS 对象层面做全量重建,然后只把差异同步到昂贵的真实 DOM 上。
3.3 虚拟 DOM 长什么样
一个虚拟 DOM 节点本质就是普通的 JS 对象:
{
type: "button",
props: {
className: "primary",
onClick: handleClick,
children: "提交"
}
}创建这样一个对象的成本,比创建一个真实 DOM 元素低两三个数量级——真实元素带着上百个属性、样式计算、布局关联。所以「先在 JS 里算一遍」是划算的。
React 拿到新旧两棵这样的对象树,逐层比较,得出一份「需要执行的最小操作列表」,比如「把第 3 个 li 的文本改成 X」「删掉第 5 个 li」,然后执行。这个比较过程叫 reconciliation(协调),第 2 章和第 12 章会深入。
Svelte 在编译期就分析出「哪个状态影响哪个 DOM 节点」,直接生成精准更新代码,运行时没有虚拟 DOM。Vue 3 结合了编译期优化和运行时 diff。SolidJS 用细粒度响应式,状态变化直接触发对应节点更新。
虚拟 DOM 只是实现声明式 UI 的一种手段,不是唯一手段,也不是最快的手段。理解它的定位,比记住「虚拟 DOM 很快」这句口号有用得多。
4. 三种范式的对比
| 维度 | 原生 / jQuery | React | Svelte |
|---|---|---|---|
| 更新方式 | 手写命令 | 状态驱动 + 运行时 diff | 状态驱动 + 编译期分析 |
| 心智负担 | 维护同步关系 | 设计状态结构 | 设计状态结构 |
| 运行时体积 | 0 | 约 45KB(gzip 前) | 极小 |
| 极限性能 | 最高(如果不写错) | 中等 | 高 |
| 大型项目可维护性 | 差 | 好 | 好 |
React 在这张表里没有一项是第一名。它赢在生态、稳定性和人才供给——这三点在工程决策里的权重,往往高于性能跑分几毫秒的差距。
5. React 不是什么
刚入门时容易高估 React 的边界,这里明确划一下:
- React 不是框架,是库。它只负责「把状态渲染成界面」。路由、数据请求、状态管理、构建工具,全都不在核心包里,需要你自己选型(第 15 到 17 章会讲)
- React 不管样式。CSS Modules、Tailwind、styled-components 都行,React 不表态
- React 不绑定平台。React DOM 渲染到浏览器,React Native 渲染到原生组件,还有渲染到终端、PDF、三维场景的渲染器。核心的协调逻辑是共用的
- React 不提供双向绑定。数据流是单向的:状态向下流,事件向上传。第 11 章会详细讲这个约束带来的好处
新手常见的挫败感来源是:学完 React 官方文档后,发现「怎么还是不会做项目」。因为做项目还需要路由、请求、表单、状态管理——这些都在 React 之外。这不是你没学好,是 React 本来就只解决了一部分问题。
6. 心智模型练习
理解 React 最好的方式是练习「把命令式需求翻译成状态」。看几个例子:
| 命令式描述 | 状态化描述 |
|---|---|
| 点击后隐藏弹窗 | 加 isOpen 状态,弹窗在 isOpen 为 true 时才渲染 |
| 请求时禁用按钮 | 加 isLoading 状态,按钮的 disabled 绑定它 |
| 切换 tab 时高亮当前项 | 加 activeTab 状态,每项的 class 由「自己是否等于 activeTab」决定 |
| 表单校验失败时显示红框 | 加 errors 状态,输入框的样式由自己是否在 errors 里决定 |
规律很清晰:每一个「界面上会变的东西」,背后都对应一个状态;界面的每一处细节,都是从状态计算出来的表达式。
一个商品详情页有这些交互:切换主图(缩略图点击)、加减购买数量(数量为 1 时禁用减号)、加入购物车(点击后按钮变成「已加入」并禁用 3 秒)、展开/收起商品详情。
请列出你需要哪些状态变量,并写出每个界面元素是如何从这些状态计算出来的。不用写代码,用中文描述即可。
下面这段命令式代码有一个「遗漏型」bug,找出它:
一个待办列表,有「添加」和「全部清空」两个操作。添加时执行:把新项 push 进数组、创建 li 节点插入 ul、更新「共 N 项」的文本。清空时执行:把数组置空、把 ul 的 innerHTML 置空。
提示:想想还有什么界面元素依赖数组长度。
判断下面几句话的对错,并说明理由:
- 虚拟 DOM 的更新速度一定比原生 DOM 操作快
- React 组件函数在一次交互中可能被执行多次
- 用了 React 就不需要理解真实 DOM 了
- 声明式的意思是「代码写得更少」
小结
- 命令式 UI 的根本问题是存在两份真相,需要人力维护同步,遗漏即 bug
- React 用「UI = f(state)」消灭了第二份真相:你只描述状态与界面的关系
- 虚拟 DOM 不是为了更快,而是为了让声明式写法在性能上可接受——在廉价的 JS 对象层做全量比较,只把差异落到昂贵的真实 DOM
- 虚拟 DOM 是实现声明式的一种手段,Svelte、Solid 用别的手段达成同样目标
- React 只是一个渲染库,路由、请求、状态管理都在核心之外
- 下一章我们拆开 JSX,看看它到底被编译成了什么 →