协调
React提供的声明式API让开发者可以在对React的底层实现没有具体了解的情况下编写应用。在开发者编写应用时虽然保持相对简单的心智,但开发者无法了解内部的实现机制。本文描述了在实现React的
diffing算法中我们做出的设计决策以保证组件满足更新具有可预测性,以及在繁杂业务下依旧保持应用的高性能性。
设计动力
在某一时间节点调用React的render()方法,会创建一颗由React元素组成的树。在下一次state或props更新时,相同的render()方法会返回一颗不同的树。React需要基于这两棵树之间的差异来判断如何由效率的更新UI,以保证当前UI与最新的树保持同步。 这个算法问题有一些通用的解决方案,即生成将一棵树转化成另一棵树的最小操作数。然而,即使在最前沿的算法中,该算法的复杂程度为O(n^3),其中n是树中元素的数量。 如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。于是React在以下两个假设的基础之上提出了一套(O(n))的启发式算法:
- 两个类型不同的元素会产生出不同的树
- 开发者可以通过
keyprop来暗示哪些子元素在不同的渲染下能保持稳定。
在实践中,以上假设在几乎所有实用场景下都成立。
Diffing算法
当对比两棵树时,React首先比较两颗树的根节点。不同类型的根节点元素会有不同的形态。
比对不同类型的元素
当根节点为不同类型的元素时,React会拆卸原有的树并且建立起新的树。举个例子,当一个元素从<a>变成<img>,从<Article>变成<Comment>或从<Buttom>变成<div>都会触发一个完整的重建流程。 当拆卸一棵树时,对应的DOM节点也会被销毁。组件实例将执行componentWillUnmount方法。当建立一棵树时,对应的DOM节点会被创建以及插入到DOM中。组件实例将执行componentWillMount方法,紧接着componentDidMount方法。所以有跟之前的树所关联的state也会被销毁。 在根节点以下的组件也会被卸载,他们的状态会被销毁,比如,当对比下面的更变时:
<div>
<Counter>
</div>
<span>
<Counter>
<span />React会销毁Counter组件并重新装载一个新的组件。
比对同一类型的元素
当比对两个相同类型的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以跟最新的元素保持一致,并且调用该实例的componentWillReceiveProps()和componentWillUpdate方法。 下一步,调用render()方法,diff算法将在之前的结果以及新的结果中进行递归。
对子节点进行递归
在默认条件下,当递归DOM节点的子元素时,React会同时遍历两个子元素的列表;当产生差异的时候,生成一个mutation。 在子元素列表末尾新增元素时,更变开销比较小。比如:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>React会先匹配两个<li></li>对应的树,然后匹配第二个元素的对应的树,最后插入第三个元素的树。 如果简单实现的话,那么列表头部插入会很影响性能,那么更变开销比较大。 React 会针对每个子元素 mutate 而不是保持相同的
Keys
为了解决以上的问题,React支持key属性。当子元素拥有key时,React使用key开匹配原有树上的子元素以及最新书上的子元素。以下例子在新增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知道只有带着2014key的元素时新元素。带着2015``2016key的元素仅仅时移动了。 现实场景中,产生一个key并不困难,你要展示的元素可能已经由了一个唯一ID,于是key可以直接从你数据中取。
<li key={item.id}>{item.name}</li>当以上情况不成立的时候,你可以新增一个id字段到你的模型中,或者利用一部分内容作为哈希值生成一个key。这个key不需要全局唯一,但是列表中需要保持唯一。 最后,你也可以使用元素在数组中的下表作为key。这个策略在元素不进行重新排序时比较合适,但是一旦由顺序修改,diff就会变得很慢。 当基于下表的组件进行重新排序时,组件state可能会遇到一些问题。由于组件实例时基于他们的key来决定是否更新以及复用,如果key是一个下表,那么修改顺序时会修改当前的key,导致非受控组件的state(比如输入框)可能相互篡改导致无法预期的变动。
权衡
协调算法是一个实现细节。React可以在每个action之后对整个应用进行重新渲染,得到最终结果也会是一样的。在此情景下,重新渲染表示在所有组件内调用render方法,这不代表React会卸载或装载它们。React只会基于以上提到的规则来决定如何进行差异的合并。 我们定期探索优化算法,让常见用理更高效的执行,在当前的实现中,可以理解为了一颗字数能在其兄弟之间移动,但是不能移动到其他位置。在这种情况下,算法会重新渲染整颗字数。 由于React依赖探索的算法,因此当以下假设没有得到满足,性能会有所损耗。
- 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但是输出非常相似的内容,建议把它们改成统一类型。在实践中,我们没有遇到这类问题。
- Key应该具有稳定,可预测,以及列表内唯一的特质。不稳定的key会导致许多组件实例和DOM节点被不必要的重新船舰,这可能导致性能下降和子组件中的状态丢失。
