React 如何根据状态变化更新页面?
先记住一句话:组件重新渲染时,React 会生成新的 React element 树,在 Render 阶段与上一棵树协调,计算出需要提交的变化,最后在 Commit 阶段更新真实宿主环境。
这里常说的“Virtual DOM”是帮助理解的概念,不是一个独立的浏览器 DOM。更准确地说,JSX 会产生 React element,Fiber 则保存协调过程中的工作单元和更新信息。
直接手动操作 DOM,需要开发者自己维护状态与 DOM 之间的对应关系:哪里新增、哪里删除、哪些属性需要更新。React 采用声明式写法:
当 count 变化时,开发者只描述“新的 UI 应该是什么样”,React 负责协调新旧 UI 并提交必要的变化。
它带来的主要价值是:
不要简单理解成“Virtual DOM 一定比手写 DOM 快”。创建 element、执行组件和协调本身也有成本;性能优势来自减少不必要的宿主环境操作,以及让 UI 更新更容易组合和调度。
可以把更新简化为两个阶段:
useEffect 通常在提交之后由 React 调度执行。Render 阶段计算出的结果不会自动等于“每个 DOM 节点都重新创建”。只有真正需要变化的部分才会进入提交过程。
如果对任意两棵树做最优的通用树编辑距离计算,复杂度会非常高。React 不追求所有情况下的最优解,而是利用 UI 的常见规律进行启发式协调:
<div> 变成 <p>,或一个组件类型变成另一个组件类型时,React 通常会销毁旧子树并创建新子树。这些规则让常见场景下的协调接近 O(n),但不能把 O(n) 当成所有更新、所有情况下的绝对保证。
对于同一位置的单个节点,可以这样理解:
| 新旧节点情况 | 典型处理 |
|---|---|
| 类型相同、key 相同 | 复用已有节点,更新 props,并继续协调子节点 |
| 类型不同 | 删除旧节点,创建新节点及其子树 |
| 新节点不存在 | 删除旧节点 |
| 旧节点不存在 | 创建新节点 |
例如:
对于组件,组件类型本身也是身份的一部分。把 UserCard 替换为 AdminCard,即使它们渲染出相似的 DOM,也不应默认认为是同一个组件实例。
key 的作用是告诉 React:同一父节点下,这个列表项的身份是什么。 它不是传给子组件的普通 prop,子组件如果需要 id,仍然要显式传入 todo.id。
列表协调时,React 会尝试根据 key 和类型复用旧节点:
当列表是静态的、只追加且顺序永不变化时,index 可能没有明显问题。但如果列表会插入、删除、排序或过滤,index 会随着位置变化,无法稳定表示数据身份:
例如列表头部插入一项后,原来的第一项会从 index 0 变成 1。React 可能复用错误的组件实例,导致输入框内容、动画状态或本地 state 跑到另一条数据上。
Key 只需要在同一个直接父节点的兄弟节点中稳定且唯一:
两个不同列表可以使用相同的 key。Key 也不应随机生成,否则每次渲染都会被视为全新节点。
Key 不只用于列表,也可以用来明确改变组件身份:
当 userId 变化时,ProfileEditor 会被视为新的组件实例,旧实例卸载,新的 state 从初始值开始。这比在 effect 中手动重置多个字段更直接,但也会同时丢弃该子树的本地状态。
不会。React element、props 和状态的处理有明确的引用与类型规则,不是对所有对象做通用深度比较。不要依赖深度比较来判断组件是否更新。
不会。Render 阶段会重新计算组件输出,但 Commit 阶段只提交必要变化;组件函数重新执行也不等于每个 DOM 节点都被销毁重建。
key 就一定能优化性能Key 的首要作用是表达身份和保证状态正确复用。稳定 key 通常也有利于减少无谓重建,但它不能替代组件拆分、列表虚拟化或性能分析。
不能。React 只根据节点类型、位置和 key 协调,不会根据对象内容猜测两个列表项是否是同一条业务数据。
| 概念 | 作用 |
|---|---|
| React element | 描述某次渲染结果的不可变对象 |
| Fiber | 保存协调工作和组件更新信息的数据结构 |
| Render | 计算并协调新的树,可能被中断或重做 |
| Commit | 将必要变化提交到 DOM 等宿主环境 |
| Diff / Reconciliation | 协调新旧树,决定复用、插入、移动或删除 |
| Key | 标识同级列表项的稳定身份 |
面试时可以这样回答:React 在 Render 阶段生成新的 element/Fiber 树,并基于类型、位置和 key 做启发式协调;Render 结果在 Commit 阶段才会应用到真实 DOM。Key 的核心不是“加速”,而是帮助 React 正确识别同级节点身份、复用状态和处理列表变化。