为社区建设略尽绵薄之力!参与 2021 社区问卷调查!
此站点不再更新。了解新站点的更多信息!

协调

这些文档已经过时且不再更新,访问 zh-hans.react.dev 查看新的 React 文档。

这些文档使用新的 React 语法,并带有在线的运行示例:

React 提供的声明式 API 让开发者可以在对 React 的底层实现并不了解的情况下编写应用。在开发者编写应用时,可以保持相对简单的心智,但开发者无法了解其内部的实现原理。本文描述了在实现 React 的 “diffing” 算法过程中所作出的设计决策,以保证组件更新可预测,且在繁杂业务场景下依然保持应用的高性能。

设计动机

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

此算法有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以使用 key 属性标识哪些子元素在不同的渲染中可能是不变的。

在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

Diffing 算法

当对比两棵树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

对比不同类型的元素

当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程。

当卸载一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount() 方法。当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 UNSAFE_componentWillMount() 方法,紧接着 componentDidMount() 方法。所有与之前的树相关联的 state 也会被销毁。

在根节点以下的组件也会被卸载,它们的状态会被销毁。比如,当比对以下更变时:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

React 会销毁 Counter 组件并且重新装载一个新的组件。

注意:

这些方法被认为是过时的,在新的代码中应该避免使用它们:

  • UNSAFE_componentWillMount()

对比同一类型的元素

当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。比如:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。

当更新 style 属性时,React 仅更新有所更变的属性。比如:

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

通过对比这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。

在处理完当前节点之后,React 继续对子节点进行递归。

对比同类型的组件元素

当一个组件更新时,组件实例会保持不变,因此可以在不同的渲染时保持 state 一致。React 将更新该组件实例的 props 以保证与最新的元素保持一致,并且调用该实例的 UNSAFE_componentWillReceiveProps()、UNSAFE_componentWillUpdate() 以及 componentDidUpdate() 方法。

下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归。

注意:

这些方法已过时,在新代码中应避免使用它们:

  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

对子节点进行递归

默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。

在子元素列表末尾新增元素时,更新开销比较小。比如:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 会先匹配两个 <li>first</li> 对应的树,然后匹配第二个元素 <li>second</li> 对应的树,最后插入第三个元素的 <li>third</li> 树。

如果只是简单的将新增元素插入到表头,那么更新开销会比较大。比如:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 并不会意识到应该保留 <li>Duke</li> 和 <li>Villanova</li>,而是会重建每一个子元素。这种情况会带来性能问题。

Keys

为了解决上述问题,React 引入了 key 属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下示例在新增 key 之后,使得树的转换效率得以提高:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut