← 返回题目列表

React Diff 算法的核心原则是什么?

高频 中等 第 6 / 27 题 更新于 2026/07/27
ReactVirtual DOMDiffkey

简化版

React Diff 基于两个假设:不同类型元素生成不同树,同层级列表用 key 标识节点身份。它只做同层比较,不做跨层级复杂移动,从而把树 diff 的复杂度降低到可接受范围。

详细版

Diff 核心规则:

  • 元素类型不同:直接卸载旧树,创建新树。
  • 元素类型相同:复用节点,更新 props。
  • 组件类型相同:保留组件实例和 state,重新渲染。
  • 列表节点依赖 key 判断身份。

示例:

items.map(item => <li key={item.id}>{item.name}</li>)

key 应该稳定、唯一。使用 index 作为 key,在插入、删除、排序时可能导致状态错位。

完整版教学

一、为什么不能做完整树编辑距离

理论上比较两棵树的最小编辑操作非常复杂,成本可能达到 O(n³)。前端 UI 更新频繁,不可能每次做这么重的计算。

React 用启发式假设降低复杂度:不同类型直接换,同层节点按 key 匹配。这牺牲了一些极端最优移动,但换来稳定可控的性能。

二、类型决定复用边界

如果 <div> 变成 <span>,React 会认为类型不同,直接替换节点。如果组件 <A /> 变成 <B />,旧组件会卸载,新组件会挂载。

如果类型相同,React 会尽量复用节点,只更新变化的属性和子节点。

三、key 决定列表身份

没有 key 或 key 不稳定时,React 只能按位置复用。列表中插入一项后,后面节点位置都变了,可能导致输入框内容、组件内部状态错位。

key 不是给开发者看的排序号,而是给 React 看的身份标识。稳定业务 id 比 index 更可靠。

四、面试追问与工程落地

常见追问是“key 改变会发生什么”。key 改变表示节点身份变了,React 会卸载旧组件并创建新组件。这可以用来主动重置表单、重新触发生命周期,但不能滥用随机 key。

还会问“虚拟 DOM 一定比直接操作 DOM 快吗”。不一定。虚拟 DOM 的价值是声明式开发、跨平台抽象和批量 diff,不是每个场景都比手写精准 DOM 操作快。复杂大列表仍然要虚拟滚动。

工程里性能问题要看组件结构、状态位置、列表 key、memo 边界,而不是简单归因于 Diff。

五、列表协调按 key 建身份映射

React 比较同一父节点下的 children,key 只需在这一组兄弟中唯一,不要求全站唯一,也不会作为普通 prop 自动传给组件。类型与 key 共同决定 Fiber 是否能复用;任何一个变化,都可能让旧状态被销毁并创建新状态。

旧:[A(key=1), B(key=2), C(key=3)]
新:[B(key=2), C(key=3), A(key=1)]

稳定 key:识别三个实体仍存在,再处理位置变化
index key:仍是 0、1、2,位置被误当成实体身份

假设 100 行带有输入状态的列表在头部插入 1 行,index key 会让原 100 个位置的“身份”都对不上业务项;稳定 id 则明确新增 1 个实体并复用原有 100 个身份。即便最终 DOM 操作经过优化,状态正确性也应先于移动次数。

items.map(item => (
  <Row key={item.id} itemId={item.id} />
))

keyitemId 在这里承担不同职责:前者只给协调器认身份,后者才是 Row 可以读取的业务 prop。把两者显式写出能避免在组件内误读 props.key

变化React 的身份判断状态结果
同位置、同 type、同 key可复用 Fiber通常保留 state
type 改变新身份旧子树卸载
key 改变新身份可主动重置 state
随机 key 每轮变化每轮全是新身份反复卸载和挂载

六、O(n) 是启发式扫描,不保证最少 DOM 移动

经典树编辑距离可能达到 O(n³),React 通过“不同类型视为不同树”和“开发者提供稳定 key”把常见协调控制在近似 O(n) 的扫描范围。这是一组工程假设,不是求所有跨层移动的全局最优解;把节点移到另一个父节点通常按删除旧节点、创建新节点处理。

Virtual DOM 的主要价值是让 UI 以声明式结果表达,并由统一协调器安排宿主变更。手写一次精准 DOM 修改当然可能更少,但应用必须自己维护所有状态与边界;遇到超长列表时,Diff 也不能消除创建数万个节点的成本,仍需要虚拟滚动或分页。

key 是状态身份契约,不是消除所有 Diff 的性能开关;先保证稳定和正确,再讨论比较与移动成本。

七、常见误区与追问

  • 误区:key 必须在整个应用中全局唯一。 它只需在同一父节点的兄弟列表中唯一并保持稳定。
  • 误区:key 会作为 props.key 传进组件。 key 是 React 协调提示;业务需要该 id 时要再传一个普通 prop。
  • 误区:React Diff 会寻找任意两棵树的全局最少编辑步骤。 它依赖类型、层级和 key 的启发式规则,换取可预测复杂度。
  • 追问:改变 key 为什么能重置表单? type/key 身份变化使旧 Fiber 卸载,新 Fiber 重新初始化 state。
  • 追问:index key 什么时候风险较低? 列表永久静态、无增删排序且节点没有跟随实体的局部状态时风险较低。
  • 追问:随机 key 为什么比不写还糟? 每轮都明确宣告所有节点换身份,缓存、DOM 和组件 state 无法复用。
  • 追问:Virtual DOM 一定比原生 DOM 快吗? 不一定,它提供声明式协调与跨平台抽象,实际性能仍取决于更新范围和节点规模。

八、加强记忆

React Diff 记住三点:不同类型直接换,同类型尽量复用,列表靠 key 认人。key 稳不稳定,决定状态会不会错位。

type 与 key 共同定义身份,O(n) 来自启发式约束,并不承诺全局最少 DOM 操作。