React 实现原理

用通俗方式理解 React 的核心实现原理。

发布于 2026年5月30日0 views

React 的核心可以简单理解成一句话:状态变化后,React 重新计算 UI,然后把真正需要变化的部分更新到页面上。

开发时我们写的是组件和状态,React 内部做的是“计算差异”和“更新 DOM”。

从组件到页面

我们写的 React 组件本质上是一个函数:

function UserCard({ name }: { name: string }) {
  return <div>{name}</div>;
}

这个函数返回的 JSX 看起来像 HTML,但它不是直接的 DOM。

JSX 会被编译成 JavaScript 对象,大概可以理解成:

{
  type: "div",
  props: {
    children: "Alice"
  }
}

也就是说,React 先拿到的是一棵“用 JavaScript 对象描述的 UI 树”。

Virtual DOM

Virtual DOM 可以理解成真实 DOM 的一份轻量描述。

真实 DOM 是浏览器里的页面节点,操作它相对昂贵。Virtual DOM 是普通 JavaScript 对象,React 可以先在内存里计算它。

流程大致是:

  1. 组件执行,生成新的 Virtual DOM。
  2. React 拿新的 Virtual DOM 和旧的 Virtual DOM 做比较。
  3. 找出真正变化的地方。
  4. 只更新对应的真实 DOM。

这就是为什么我们写 React 时通常只关心状态,不需要手动操作 DOM。

状态驱动更新

React 页面更新通常从 state 变化开始。

const [count, setCount] = useState(0);

function increment() {
  setCount((value) => value + 1);
}

调用 setCount 后,React 不会让你手动改按钮文字,而是重新执行组件函数:

return <button>{count}</button>;

新的 count 会生成新的 UI 描述,React 再决定页面上哪些地方需要更新。

所以 React 的思路是:

UI = f(state)

状态变了,UI 就重新计算。

Diff

Diff 是 React 比较新旧 UI 树的过程。

比如旧 UI:

<div>
  <span>Alice</span>
</div>

新 UI:

<div>
  <span>Bob</span>
</div>

React 会发现结构没变,只是文本从 Alice 变成了 Bob,于是只更新文本内容。

如果组件类型变了:

<UserCard />

变成:

<OrderCard />

React 通常会认为这是两种不同的组件,旧组件卸载,新组件重新创建。

key 的作用

列表渲染时,key 用来帮助 React 判断“哪个元素是谁”。

users.map((user) => (
  <UserItem key={user.id} user={user} />
));

如果没有稳定的 key,React 很难准确判断列表项的增删移动,可能导致错误复用组件状态。

不推荐用数组下标做 key:

users.map((user, index) => (
  <UserItem key={index} user={user} />
));

当列表顺序变化时,index 会变得不稳定。更推荐使用业务 ID。

Fiber

Fiber 是 React 内部的任务调度结构。

可以把它理解成 React 给每个组件节点建立的一份“工作记录”。这份记录里会保存:

  • 当前组件是什么。
  • 当前 props 和 state。
  • 子节点、父节点、兄弟节点关系。
  • 这次更新需要做什么。

为什么需要 Fiber?

因为页面更新可能很大,如果 React 一口气把所有更新都做完,浏览器可能会卡住。Fiber 让 React 可以把更新拆成很多小任务,根据优先级安排执行。

比如:

  • 用户输入、点击:优先级高。
  • 大列表渲染:优先级低一些。
  • 不紧急的页面内容:可以稍后处理。

这也是 React 能支持并发渲染的基础。

Render 和 Commit

React 更新大致分两个阶段:

Render 阶段

Render 阶段负责计算。

React 会执行组件,生成新的 UI 描述,比较差异,整理出需要更新的内容。

这个阶段可以被暂停、丢弃或重新开始,因为它还没有真正改 DOM。

Commit 阶段

Commit 阶段负责提交。

React 会把 Render 阶段算出来的变化真正应用到 DOM 上,并执行一些副作用。

比如:

  • 插入 DOM。
  • 更新文本。
  • 设置属性。
  • 执行 useLayoutEffect
  • 安排 useEffect

可以简单记:

  • Render:算出要改什么。
  • Commit:真正改页面。

Hooks 为什么要按顺序调用

React Hooks 依赖调用顺序来保存状态。

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("Alice");
}

React 内部不是靠变量名识别这两个 state,而是靠“第一次 Hook、第二次 Hook”这样的顺序。

所以 Hooks 不能写在条件语句里:

if (show) {
  const [count, setCount] = useState(0);
}

如果某次渲染执行了这个 Hook,另一次渲染没执行,Hook 顺序就乱了,React 就不知道哪个状态对应哪个 Hook。

正确做法是:Hooks 永远放在组件顶层调用。

useEffect 什么时候执行

useEffect 用来处理渲染之后的副作用。

useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);

它通常在页面更新到浏览器之后执行。

适合放:

  • 请求数据。
  • 订阅事件。
  • 设置定时器。
  • 手动操作非 React 管理的外部系统。

如果一个值可以直接通过 props 或 state 算出来,通常不需要放进 useEffect

为什么 setState 后拿不到最新值

React 的状态更新不是立即同步改变量,而是安排一次更新。

setCount(count + 1);
console.log(count);

这里打印的 count 可能还是旧值,因为当前这次渲染里的 count 已经固定了。

如果下一次状态依赖上一次状态,推荐使用函数写法:

setCount((value) => value + 1);

这样 React 会基于最新状态计算。

批处理

React 会把多次状态更新合并处理,减少重复渲染。

setCount((value) => value + 1);
setName("Bob");
setVisible(true);

这些更新通常会被合并成一次渲染,而不是每调用一次 setState 就渲染一次。

这能减少性能浪费。

React.memo、useMemo、useCallback 的位置

这几个 API 都和“减少不必要计算或渲染”有关。

  • React.memo:缓存组件,props 没变就跳过子组件渲染。
  • useMemo:缓存计算结果。
  • useCallback:缓存函数引用。

它们不是必须默认使用的工具。项目里应该先保证组件结构清楚,再根据实际性能问题使用。

总结

React 的实现可以理解成:组件返回 UI 描述,状态变化后生成新的 UI 描述,React 通过 Diff 找出变化,再通过 Fiber 调度更新,最后在 Commit 阶段把变化应用到真实 DOM。开发时我们写组件和状态,React 负责把状态变化高效地同步到页面。