前言
React通过构建React element tree(即Virtual DOM或者Fiber Tree,详见文章:Virtual DOM and Internals )及render函数将数据映射到真实DOM上,而每state或者prop更新,调用render函数,其均会返回一棵不一样的React element tree,接着React会对比前后的React element tree,发现两颗树的不同点,然后以最小的损耗更新真实DOM。
React对比前后的React element tree,发现其不同点的过程即为diff算法。
本文基于官方文章:Reconciliation ,及累积知识做总结。
传统diff与 React diff
官网上的传统diff算法其时间复杂度太高,其为n的3次方。如果在React中直接使用此算法,当展示的元素在1000时,其量级会达到十亿,这是非常昂贵的。
所以React基于以下两个假设改进了diff算法:
- 不同类型的两个元素会产生两颗不同的树
- 开发者可使用key prop暗示哪些子元素在不同的渲染下保持稳定
接下来解释React的diff算法,主要有以下几种情况

React diff
当比较两颗树时,React会先比较其根节点,然后根据不同的情况做出不同的行为。
元素类型不同
如果前后的元素类型不同,React会直接销毁原来的树,并在销毁处新建一棵树。
比如从<a>变换至<img>,从<Article>变换至<Comment>,从<Button>变换至<div>,这类不同类型的元素会直接重建使其重建。
当销毁一棵旧树时,DOM节点会被销毁,组件实例会执行componentWillUnmount();当建立一棵新树时,新的DOM节点被插入DOM,组件实例会执行componentWillMount()和componentDidMount()。旧树的state都会丢失。
即便是如下类型的结构也会导致重建:
<div>
<Counter />
</div>
变换为
<span>
<Counter />
</span>
原来的Counter会被销毁,然后重建
相同类型DOM元素
当比较React中的两个DOM元素时,React会比较它们的属性,保持原先的DOM节点,然后仅仅更新改变了的属性。
举例
<div className="before" title="stuff" />
变换为
<div className="after" title="stuff" />
React知道仅仅需要更改className。
若是style属性
<div style={{color: 'red', fontWeight: 'bold'}} />
变换为
<div style={{color: 'green', fontWeight: 'bold'}} />
也是仅仅修改color。
在操作完成后,React会继续递归对应DOM节点的子元素。
相同类型组件元素
当一个组件更新时,其实例状态是不变的,所以state在跨越多个render时保持一致。React会更新组件实例的props,以匹配新的元素,同时在依赖prop的实例上调用componentWillReceiveProps()和componentWillUpdate()。
接下来render方法继续调用,diff算法会递归比较老树和旧树。
递归子元素及key的作用
默认情况下,递归DOM节点的多个子元素时,React会同时遍历前后的两个子元素列表,当其有不同时对其进行改变。
举例子,比如想要在子节点末端加入元素:
<ul>
<li>first</li>
<li>second</li>
</ul>
变换后:
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React后先匹配first,再匹配second,然后插入third。
但是假设不做任何处理,在子元素前端插入节点就会产生非常差的性能,举例子:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
变换后
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
欲在如上之中前端插入Connecticut,若不做任何处理,React会改变每一个子元素,而不是保持原来的<li>Duke</li>和<li>Villanova</li>序列。这会造成严重的性能问题。
为了解决这个问题,React使用key属性。当子元素有key时,React会使用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,因为其是不稳定的。假设id使用Math.random()生成,按上面举例中,所有子元素极大可能因为前后ID不相同均被重新创建。
总结
在了解react的diff机制之后,对编写代码能有更好地规范和组织,对一些bug问题、性能问题能更好地定位,对生命周期能更有效的理解。
本文基本是根据官网原文进行总结和翻译,若有不足之处,还请大佬们指教。
参考资料
react官网Reconciliation
Virtual DOM and Internals
知乎——浅析React Diff 与 Fiber