Skip to content

React 渲染、diff、Fiber 与更新过程 ​

很多人提 React 原理时,最容易把这些词堆在一起:

  1. 虚拟 DOM
  2. diff
  3. Fiber
  4. 调和
  5. 渲染

但如果不把它们放回同一条主线上,就很容易只剩零散定义。

这篇文章重点讲:

  1. React 更新一轮 UI 时到底发生了什么
  2. 虚拟 DOM 解决的是什么问题
  3. diff 在比较什么
  4. key 为什么重要
  5. Fiber 为什么不是“另一个虚拟 DOM”

1. React 更新 UI 的主线是什么 ​

可以把 React 的一次更新压缩成 3 个阶段:

  1. render:根据最新状态生成新的 React Element 树
  2. reconcile:把新旧树做比较,找出要变动的部分
  3. commit:把最终变更提交到真实宿主环境,例如浏览器 DOM

把这条主线拉开:

  1. React 不是一改 state 就立刻改 DOM
  2. React 会先算“新的 UI 应该长什么样”
  3. 再比较差异
  4. 最后统一提交

这条主线最适合用来串起后面的虚拟 DOM、diff 和 Fiber。


2. 虚拟 DOM 到底是什么 ​

虚拟 DOM 不是浏览器里的真实 DOM。

它本质上是一种 JavaScript 层的 UI 描述模型,用来表达:

  1. 节点类型是什么
  2. 节点上有哪些属性
  3. 子节点结构是什么

例如:

jsx
function App() {
  return (
    <div className="page">
      <h1>Hello</h1>
      <button>Click</button>
    </div>
  );
}

这段 JSX 经过编译后,最终会变成一棵描述型对象树,而不是直接变成真实 DOM。

🌟 虚拟 DOM 最准确的价值,不是“一定更快”,而是 让 UI 更新过程可以在 JavaScript 层计算和比较,再统一落到宿主环境。


3. 什么叫调和(Reconciliation) ​

调和可以看成 React 在新旧两棵 UI 描述树之间寻找差异,并决定哪些部分要复用、哪些部分要更新。

也就是说,调和回答的是:

  1. 哪些节点还是原来的
  2. 哪些节点需要替换
  3. 哪些节点只是属性变了
  4. 哪些子节点顺序变化了

diff 只是调和过程里最常被单独提出来的一部分。


4. React 的 diff 在比较什么 ​

React 不会拿整棵树做最暴力的通用最优算法比较。

工程上它采用的是一套更可落地的启发式策略。

最值得先记住的是这几条:

4.1 不同类型的节点,通常直接认为不是同一个东西 ​

例如:

jsx
<div />

变成:

jsx
<section />

React 更可能直接把它看成旧节点卸载、新节点挂载,而不是在原地“智能变形”。

4.2 同层比较非常重要 ​

React 更强调同一层级下节点的比较,而不是跨层随意做最优迁移。

4.3 key 用来标识同层节点身份 ​

这就是为什么列表渲染里 key 非常关键。


5. key 为什么会影响状态和性能 ​

很多人以为 key 只是为了消除警告。

其实它的真正作用是,告诉 React,同层节点里谁是谁。

例如:

jsx
{
  users.map(user => <UserRow key={user.id} user={user} />)
}

这里的稳定 key 可以帮助 React:

  1. 更准确地复用节点
  2. 避免不必要的销毁和重建
  3. 避免列表项状态错位

反过来,如果你写成:

jsx
{
  users.map((user, index) => <UserRow key={index} user={user} />)
}

当列表发生插入、删除、重排时,就更容易出现:

  1. 输入框状态串位
  2. 动画错位
  3. 不必要的重新挂载

🌟 所以 key 最重要的价值不是“提高一点点性能”,而是 帮助 React 正确识别节点身份,保留该保留的状态。


6. 一段列表重排示例 ​

下面这个例子最适合帮助理解 key 的意义。

jsx
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoItem({ todo }) {
  const [editing, setEditing] = useState(false);

  return (
    <li>
      <span>{todo.title}</span>
      <button onClick={() => setEditing(prev => !prev)}>
        {editing ? "完成编辑" : "编辑"}
      </button>
    </li>
  );
}

这里如果 key 稳定,列表重排时,React 更容易把“这条待办对应的本地编辑状态”保留下来。

如果 key 不稳定,React 可能把原本属于 A 行的状态误套到 B 行。


7. commit 阶段在做什么 ​

很多人理解到 diff 就停了,但 React 还有 commit 阶段。

commit 可以看成 把前面计算好的变更真正应用到宿主环境。

在浏览器里,这一步通常包括:

  1. 创建、更新、删除 DOM 节点
  2. 绑定或解绑事件
  3. 执行布局相关副作用
  4. 处理 ref 挂载和卸载

这也是为什么很多 Hook 会围绕 commit 前后时机展开,例如:

  1. useEffect
  2. useLayoutEffect
  3. ref 更新

8. Fiber 到底是什么 ​

Fiber 不是“另一个虚拟 DOM”,也不是“一个新的 diff 算法名字”。

Fiber 是 React 内部的更新任务组织和调度模型。

它主要解决的是:

  1. 更新工作怎么拆分
  2. 哪些更新优先级更高
  3. 一次大更新能不能中断、恢复、继续
  4. 如何避免长时间阻塞主线程

在 Fiber 出现之前,你可以把 React 更新近似理解成“一次性做完一大块工作”。
Fiber 则把它变成了“可拆分、可调度、可中断的一系列小任务”。


9. 为什么 Fiber 很重要 ​

Fiber 重要,不是因为它让 React 这个词更“底层”,而是因为很多现代 React 能力都建立在它之上。

例如:

  1. 更细粒度更新调度
  2. 更合理的任务优先级
  3. 并发相关能力
  4. 更平滑的用户交互响应

把这几个词放回各自位置看:

  • 虚拟 DOM 更偏“描述 UI”
  • diff 更偏“比较差异”
  • Fiber 更偏“组织更新工作并调度执行”

10. 一张总图把关系串起来 ​

mermaid
flowchart TD
    A[state 或 props 变化] --> B[函数组件重新执行]
    B --> C[生成新的 React Element 树]
    C --> D[调和 reconcile]
    D --> E[diff 新旧节点]
    E --> F[生成变更列表]
    F --> G[commit]
    G --> H[更新 DOM 与副作用]

    D --> I[Fiber 调度与任务拆分]

这张图最重要的作用,是把 3 件容易混的事分开:

  1. UI 描述怎么生成
  2. 差异怎么比较
  3. 更新工作怎么调度

11. React 渲染原理里最常见的误区 ​

11.1 以为虚拟 DOM 的核心结论就是“更快” ​

这太绝对了。

它更核心的价值是统一 UI 描述和更新模型。

11.2 以为 diff 是全量最优算法 ​

不是。

React 用的是工程上更可控的启发式比较策略。

11.3 以为 Fiber 就是虚拟 DOM 2.0 ​

也不对。

Fiber 的重点是更新任务的拆分与调度。

11.4 以为 key 只是为了警告消失 ​

它真正影响的是节点身份和状态保留。


12. 一句话总结 ​

React 的渲染原理主线,本质上是 先用 React Element 描述 UI,再通过调和和 diff 找出变化,最后在 commit 阶段更新真实环境,而 Fiber 负责把这套更新工作拆分并调度起来。

基于 VitePress 构建的个人技术笔记。