Learn
React/01-what-is-react

React 是什么

💡🤖 AI 时代,还要学 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);

这样做功能上完全正确,但有三个致命问题:

  1. 性能灾难。重建 DOM 是昂贵操作,输入框里的每一次按键都要重建整页
  2. 状态丢失。输入框的光标位置、滚动条位置、正在播放的视频,全都会被重置
  3. 动画中断。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 章会深入。

ℹ️不是所有框架都用虚拟 DOM

Svelte 在编译期就分析出「哪个状态影响哪个 DOM 节点」,直接生成精准更新代码,运行时没有虚拟 DOM。Vue 3 结合了编译期优化和运行时 diff。SolidJS 用细粒度响应式,状态变化直接触发对应节点更新。

虚拟 DOM 只是实现声明式 UI 的一种手段,不是唯一手段,也不是最快的手段。理解它的定位,比记住「虚拟 DOM 很快」这句口号有用得多。

4. 三种范式的对比

维度原生 / jQueryReactSvelte
更新方式手写命令状态驱动 + 运行时 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 当成 Angular 那样的全家桶

新手常见的挫败感来源是:学完 React 官方文档后,发现「怎么还是不会做项目」。因为做项目还需要路由、请求、表单、状态管理——这些都在 React 之外。这不是你没学好,是 React 本来就只解决了一部分问题。

6. 心智模型练习

理解 React 最好的方式是练习「把命令式需求翻译成状态」。看几个例子:

命令式描述状态化描述
点击后隐藏弹窗加 isOpen 状态,弹窗在 isOpen 为 true 时才渲染
请求时禁用按钮加 isLoading 状态,按钮的 disabled 绑定它
切换 tab 时高亮当前项加 activeTab 状态,每项的 class 由「自己是否等于 activeTab」决定
表单校验失败时显示红框加 errors 状态,输入框的样式由自己是否在 errors 里决定

规律很清晰:每一个「界面上会变的东西」,背后都对应一个状态;界面的每一处细节,都是从状态计算出来的表达式。

🎯练习 1:翻译成状态

一个商品详情页有这些交互:切换主图(缩略图点击)、加减购买数量(数量为 1 时禁用减号)、加入购物车(点击后按钮变成「已加入」并禁用 3 秒)、展开/收起商品详情。

请列出你需要哪些状态变量,并写出每个界面元素是如何从这些状态计算出来的。不用写代码,用中文描述即可。

🎯练习 2:找出同步 bug

下面这段命令式代码有一个「遗漏型」bug,找出它:

一个待办列表,有「添加」和「全部清空」两个操作。添加时执行:把新项 push 进数组、创建 li 节点插入 ul、更新「共 N 项」的文本。清空时执行:把数组置空、把 ul 的 innerHTML 置空。

提示:想想还有什么界面元素依赖数组长度。

🎯练习 3:判断题

判断下面几句话的对错,并说明理由:

  1. 虚拟 DOM 的更新速度一定比原生 DOM 操作快
  2. React 组件函数在一次交互中可能被执行多次
  3. 用了 React 就不需要理解真实 DOM 了
  4. 声明式的意思是「代码写得更少」

小结

  • 命令式 UI 的根本问题是存在两份真相,需要人力维护同步,遗漏即 bug
  • React 用「UI = f(state)」消灭了第二份真相:你只描述状态与界面的关系
  • 虚拟 DOM 不是为了更快,而是为了让声明式写法在性能上可接受——在廉价的 JS 对象层做全量比较,只把差异落到昂贵的真实 DOM
  • 虚拟 DOM 是实现声明式的一种手段,Svelte、Solid 用别的手段达成同样目标
  • React 只是一个渲染库,路由、请求、状态管理都在核心之外
  • 下一章我们拆开 JSX,看看它到底被编译成了什么 →