从一个问题开始
在讨论 React 之前,先想一个问题:如果没有 React,你打算怎么写一个带交互的页面?
可能是这样:页面上有一个按钮,点击之后按钮旁边的数字加一。没有 React 的时候,你会这样写:
<button id="btn">点击</button>
<span id="count">0</span>
let count = 0;
document.getElementById('btn').addEventListener('click', () => {
count += 1;
document.getElementById('count').textContent = count;
});
这个写法完全正常,能 work,也很好理解。如果页面上只有一个交互逻辑,这样写完全没问题。
但如果页面上有几十个按钮、几十个状态、几十个交互逻辑呢?如果这些状态之间有依赖关系呢?如果某个状态变了之后需要同时更新页面上多个地方呢?
命令式的 DOM 操作会迅速变成一团乱麻:状态和 DOM 分开管理,状态变的时候需要手动同步 DOM,漏掉一个同步就会产生 bug,排查起来非常困难。
React 解决的就是这个问题:如何让 UI 程序员在面对复杂交互的时候,不需要手动管理状态和 DOM 的同步关系。
声明式 UI 的核心思想
React 提出了一个很直接的想法:与其告诉 JavaScript "去改这个 DOM 节点",不如告诉 React"我现在的状态是这样的,帮我渲染出对应的 UI"。
这就是声明式 UI 的核心:描述"应该是什么样",而不是描述"怎么去改"。
用 React 的写法,上面那个计数器会变成这样:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击 {count}
</button>
);
}
你不需要管 setCount 之后 DOM 怎么更新,你只需要说"当 count 是 5 的时候,按钮上应该显示数字 5"。React 负责在 count 变化的时候自动更新 DOM。
这意味着当页面上有几十个关联状态时,你不需要写几十条手动 DOM 更新语句。你只需要把状态之间的关系描述清楚,React 会自动处理同步。
为什么命令式 DOM 操作是个坑
命令式代码的本质是:描述每一步操作,然后让计算机执行。当操作步骤少的时候,命令式代码清晰、直观。但当操作步骤变多、步骤之间的关系变复杂时,命令式代码会遇到几个经典问题。
状态和 DOM 不同步。 状态存在 JavaScript 变量里,DOM 存在页面里,两者之间需要手动同步。当状态变的时候,忘了更新某个 DOM 节点,就会产生 UI 和数据不一致的 bug。这种 bug 很难复现和排查,因为代码逻辑本身没有错,只是漏了一步。
交叉状态的处理非常复杂。 假设页面上有 A、B、C 三个状态,它们之间相互影响:A 变了 B 和 C 要跟着变;B 变了 A 和 C 要跟着变;C 变了 A 和 B 要跟着变。命令式写法需要你在每一步操作里都把相关的更新写出来,很容易漏掉某个分支,也很难验证完整性。
并发修改很难处理。 如果页面上同时有两个事件处理器在运行,它们可能同时修改同一个状态。命令式代码很难保证这类竞态问题不会发生。
React 通过把状态和 DOM 绑定在一起,用"状态变化自动触发 UI 更新"的机制,从根本上规避了这些问题。
组件:UI 抽象的单位
React 引入的第二个核心概念是组件。
组件是把一段 UI 封装起来的方式。它有自己的状态,有自己的渲染逻辑,外部不需要知道它内部是怎么工作的,只需要给它传入合适的 props,它就会渲染出对应的 UI。
组件化带来的直接好处是复用。同一个按钮组件可以在不同地方使用,每次使用只需要传不同的 props,不用重复写样式和行为。
更深一层的好处是抽象。组件内部可以很复杂,但组件的接口是简单的。把复杂性封装在组件内部,让使用组件的地方保持简单,这是前端工程化的核心思想。
// 使用者不需要知道 Button 内部是怎么处理点击的
function App() {
return (
<div>
<Button label="提交" onClick={handleSubmit} />
<Button label="取消" onClick={handleCancel} />
</div>
);
}
单向数据流:状态变化的可预测性
React 的第三个核心设计是单向数据流。
在 React 里,数据只能从父组件流向子组件,不能反向传播。子组件如果想要改变父组件的状态,只能通过回调函数的方式,让父组件来处理这个请求。
这个设计看起来增加了复杂性,但带来的好处是:任意时刻,你总能找到一个明确的数据来源——就是最近的持有这个状态的父组件。当状态变化的时候,你不需要追踪"谁可能改了这个状态",只需要看这个状态的拥有者。
单向数据流让状态变化变得可追踪、可预测。这是 React 应用在复杂场景下依然能保持可控的根本原因。
React 的本质:状态到视图的映射
React 的所有设计——声明式 UI、组件化、单向数据流——都是围绕一个核心问题展开的:如何让"状态变化自动产生正确的 UI 更新"这件事变得可工程化。
在 React 之前,前端 UI 编程的本质是:管理 DOM 节点和它们的操作序列。React 把这个问题变成了:管理状态,以及状态到 UI 的映射关系。
这个转变带来的变化是根本性的。当 UI 变成状态的函数之后,你不再需要手动追踪"改了状态之后哪个 DOM 要更新"。React 的渲染机制会自动帮你做这件事。你的工作变成了"描述状态和 UI 的关系",而不是"描述每一步 DOM 操作"。
React 不是银弹
理解了 React 解决的问题,也要理解它带来的新问题。
React 没有解决"状态应该怎么组织"。当应用变复杂的时候,状态会变多,状态之间的关系会变复杂,这时候怎么组织状态就成了新的挑战。Redux、Zustand、Jotai 这些状态管理方案,以及 React 自己的 Context、useReducer,都是在尝试解决这个问题。
React 也没有让"UI 逻辑"本身消失。组件内部的状态逻辑、副作用逻辑、边界情况处理,这些复杂性依然存在,只是换了一种组织方式。
React 解决的是"状态和 UI 同步"这个问题,但不是所有前端问题。理解这一点,才能在合适的场景下用它,而不是把它当成解决一切的万能方案。
这一章想说的
React 解决的根本问题不是"怎么写 UI",而是"怎么让复杂交互场景下的状态和 DOM 同步变得可工程化"。
围绕这个核心问题,React 引入了声明式 UI(描述"应该是什么样"而不是"怎么改")、组件化(封装复杂性的抽象单位)、单向数据流(让状态变化可追踪)。
理解 React 的设计动机,比单纯记住 API 重要得多。它决定了你能不能在复杂场景下正确使用 React,也决定了你能不能理解 React 后续版本引入的各种新特性和优化背后的原因。