React版本:15.4.2
**翻译:xiyoki **
React提供了一个声明式API,所以你不必担心每次更新的确切更改。这使得编写应用程序更容易,但是在React中更新是如何实现的可能不明显。本文解释了我们在React的‘diffing’算法中做出的选择,以便组件更新是可预测,同时高性能应用程序也足够快。
Motivation
当你使用React时,在单个时间点,你可以将该render()
函数想象为创建一个React元素树。在下一个state或props更新时,该render()
函数将返回一个不同的React元素树。React然后需要找出如何有效地更新UI以匹配最新的树。
对于生成将一个树变换成另一个树的最小操作数的算法问题,存在一些通用解决方案。然而, state of the art algorithms具有大约 O(n3)的复杂性,其中n是树中的元素数量。
如果我们在React中使用它,显示1000个元素将需要大约十亿此比较。这太贵了。相反,React基于两个假设实现启发式O(n)算法:
- 不同类型的两个元素将产生不同的树。
- 开发者可以暗示,在具有key prop的不同渲染之间,哪些子元素是稳定的。
在实践中,这些假设对于几乎所有实际使用情况都是有效的。
The Diffing Algorithm(差分算法)
当差分两棵树时,React首先比较两个根元素。根据根元素的类型,行为是不同的。
Elements Of Different Type(不同类型的元素)
每当根元素具有不同类型时,React将拆除旧树并从头开始构建新树。从<a>
到 <img>
, 或从<Article>
到<Comment>
, 或从<Button>
到<div>
- 任何这些将导致完全重建。
当拆除树时,旧的DOM节点被销毁。组件实例接收componentWillUnmount()
。当构建新树时,将新的DOM节点插入到DOM中。组件实例接收componentWillMount()
,然后componentDidMount()
。与旧树相关联的任何状态都将丢失。
根下面的任何组件也将被卸载并且它们的状态被销毁。例如,当差分时:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
这将破坏旧的Counter
,并重新安装一个新的。
DOM Elements Of The Same Type(相同类型的DOM元素)
当比较相同类型的两个React DOM元素时,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知道只修改color
样式,而不是修改fontWeight
样式。
处理DOM节点后,React然后对子节点进行递归。
Component Elements Of The Same Type(相同类型的组件元素)
当组件更新时,实例保持不变,so that state is maintained across renders。React更新底层组件实例的props以匹配新元素,并且在底层实例上调用 componentWillReceiveProps()
和 componentWillUpdate()
。
接下来,render()
方法被调用,diff算法对前一个结果和新结果进行递归。
Recursing On Children(在子元素上递归)
默认情况下,当对DOM节点的子节点进行递归时,React只是同时迭代这两个字节点列表,并在有差异时生成一个变量。
例如,当在子元素的末尾添加一个元素时,这两个数之间的转换效果很好:
<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将改变每个child,而不是意识到它课可以保持<li>Duke</li>
和<li>Villanova</li>
子树完好无损。这种低效率会是一个问题。
Keys
为了解决这个问题,React支持一个key
属性。当child有key属性时,React使用key将原始树中的child与后续树中的child进行匹配。例如,添加key
到上面低效的示例可以使树有效地转换:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
现在React知道key为‘2014’的元素是新的,而key为'2015'和‘2016’的元素只是移动了一下。
在实践中,寻找key通常不难。你要显示的元素可能已具有唯一的ID,因此key可以来自你的数据:
<li key={item.id}>{item.name}</li>
如果不是这样,你可以向模型中添加一个新的ID属性,或hash内容的某些部分以生成key。
key只需在其兄弟之间是唯一的,而不是全局唯一的。
作为最后一种手段,你可以将数组中的项目索引作为key。这可以很好地工作,如果项目从来没有重新排序,但重新排序会很慢。
Tradeoffs(权衡)
重点记住,reconciliation算法是一个实现细节。React可以在每个action上重新渲染整个应用程序;最终结果将是相同的。我们经常细化启发式算法,以便使常见用例更快。
在当前的实现中,你可以表达这个事实,一个子树已经被移动到它的兄弟姐妹中,但你不知道它已经被移动到别的地方。该算法将重新渲染该完整子树。
因为React依赖于启发式算法,如果不能满足该算法设定的假设,性能将受到影响。
- 该算法不会尝试匹配不同组件类型的子树。如果你发现自己在两个具有非常相似输出的组件类型之间徘徊,你应该使它们类型相同。在实践中,我们没有发现这是一个问题。
- key应该是稳定、可预测和唯一的。不稳定的key(如由
Math.random()
产生的)将导致许多组件实例和DOM节点被不必要地重新创建,这可能导致子组件性能降级和丢失状态。