React diff机制

966 阅读5分钟

前言

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算法:

  1. 不同类型的两个元素会产生两颗不同的树
  2. 开发者可使用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