调和是什么
调和(Reconciliation)是 React 用来对比两棵树的算法,决定哪些部分需要更新,哪些可以复用。
当你调用 setState 时,React 会生成一棵新的 React Element 树,然后和上一棵渲染在内存中的树做对比。这个对比过程就是调和。
用 Andrew Clark 在 React Fiber Architecture 文档里的话说:"The algorithm React uses to diff one tree with another to determine which parts need to be changed."1
但 Fiber 之前和 Fiber 之后的调和实现有本质区别。React 15 的调和是递归的、同步的,不能中断。Fiber 重写之后,调和变成了可中断的工作单元,可以在执行过程中被更高优先级的任务打断。
Diff 策略的基本假设
React 的 Diff 算法建立在两个假设上。这两个假设是性能优化的关键,但也意味着在某些场景下你需要主动配合 React,才能获得最佳性能。
假设一:不同类型的组件产生不同的树。 React 会认为如果一个位置上是 <div> 变成 <span>,那么整个子树都需要被替换,而不是去比较它们的子节点是否相同。
// 旧树
<div>
<span>old content</span>
</div>
// 新树
<span>
<span>new content</span> // 这里虽然是 span,但它在 React 眼里是完全不同的子树
</span>
React 不会尝试复用第一个 <span>,而是直接卸载旧的子树、挂载新的子树。这意味着稳定组件类型能帮助 React 复用已有节点。
假设二:开发者可以通过 key 来暗示哪些子元素是稳定的。 对于列表,React 假设有相同 key 的元素是同一个,不需要重新创建。
// 使用 key 帮助 React 识别稳定元素
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
没有 key 或 key 不稳定(如使用索引)会导致 React 认为每个列表项都是新创建的,可能引发性能问题和状态丢失。
三个场景下的 Diff 策略
场景一:两个不同类型的元素
// 之前
<div><span>old</span></div>
// 之后
<span><span>new</span></span>
React 会认为 <div> 和 <span> 完全不同,直接卸载整个旧子树(包括 span),挂载新的。这个过程会触发组件的卸载和挂载生命周期函数。
这引出一个常见的性能优化原则:保持 DOM 节点类型稳定,避免不必要的容器类型切换。比如把 <div> 换成 <section> 会导致整棵子树重建。
场景二:同类型 DOM 元素
// 之前
<div className="before" style={{color: 'red'}} />
// 之后
<div className="after" style={{color: 'blue'}} />
React 会认为这是同一个 <div>,只更新变化的属性。className 从 "before" 变成 "after",style 的 color 从 red 变成 blue。不会重新创建 div 节点,只会应用必要的 DOM 属性变更。
这是 React 更新 DOM 的常见模式:尽量复用同类型节点,只变更必要的属性。
场景三:同类型组件元素
// 之前
<Parent>
<Child name="old" />
</Parent>
// 之后
<Parent>
<Child name="new" />
</Parent>
组件类型相同但 props 不同,React 会认为这是同一个组件实例,会触发这个组件的更新流程:
- 组件实例保持不变
- 组件的
props更新 - 组件的
render方法被调用 - Diff 算法继续对比子树的返回结果
key 的正确使用方式
列表渲染中的 key 看起来是个小问题,但踩坑的人不少。
不要使用数组索引作为 key,除非你确定列表不会变化(插入、删除、排序):
// 危险:使用索引作为 key
items.map((item, index) => (
<li key={index}>{item.name}</li>
))
当列表中间插入一个新项时,所有现有元素的索引都会变化,React 会认为它们都是新的——这可能导致状态混乱和性能下降。
使用稳定、唯一、可预测的 ID 作为 key:
// 推荐:使用唯一 ID
items.map(item => (
<li key={item.id}>{item.name}</li>
))
key 只需要在兄弟元素之间唯一,不需要全局唯一。同一层级的 li 之间 key 不能重复,但不同层级的 key 可以相同。
Fiber 架构下的调和变化
在 Fiber 架构下,调和不再是一次性完成的递归过程,而是被拆分成了多个工作单元。
每个 Fiber 节点都有 effectTag 标记,表示这个节点需要执行什么类型的更新:Placement(插入)、Update(更新)、Deletion(删除)等。
Render Phase 阶段,React 遍历 Fiber 树,标记每个有变化的节点。Commit Phase 阶段,React 根据 effectTag 一次性应用所有 DOM 变更。
这就是为什么 React 17 引入的 Concurrent Mode 能实现"可中断渲染":调和被拆成小单元后,浏览器可以在这些单元之间插入其他任务,不会出现长时间阻塞。
调和和渲染的关系
调和(Reconciliation)和渲染(Rendering)是两个不同的概念:
- 调和:决定哪些 parts 需要更新,这是纯 JavaScript 计算
- 渲染:把变更应用到 DOM 上,这会实际改变浏览器页面
React DOM 和 React Native 能共享同一个调和器,但使用不同的渲染器。调和算法本身不关心最终输出到哪里,只关心"怎么把旧树变成新树"。这个分离是 React 多平台支持的基础。
这一章想说的
调和是 React 对比新旧两棵 React Element 树、决定哪些需要更新的过程。Diff 算法建立在两个假设上:不同类型元素产生不同树,相同 key 暗示子元素稳定。
React 通过这 三个 Diff 策略(不同类型元素、同类型 DOM 元素、同类型组件元素)实现了高效的树对比。但这些策略依赖开发者的配合:保持 DOM 类型稳定、使用唯一稳定的 key。
Fiber 架构让调和变成了可中断的工作单元,为 React 的并发渲染奠定了基础。