react-VirtualDOM和Diff算法

前言

VirtualDOM和Diff算法 是react核心的原理。
原理实现: 用js中的狭义对象(obj)表示所有的DOM节点,所以VirtualDOM 是js中的一个狭义对象。在react中,大体上就是围绕实际对象和VirtualDOM这俩个狭义对象进行各种操作(可以看做是”类”里面的静态方法属性),比如,对比俩者之间的差异操作,根基实际对象,渲染实际DOM的操作。
Diff算法,其实就是一种基于假设的LCS的一种实现。

VirtualDOM

是一种算法思想,或者说是设计模式。在js中本质上就是一个js对象。在其他任何语言里都可以实现VirtualDOM。

Diff算法

一种算法。重点是react进行假设,我感觉其实就是“动态规划”的LCS嘛。

假设前提

  • 不同类型的元素生成不同的节点树

  • 开发者通过key属性的不同表现指出哪个子元素应该渲染。

    如何比较?

    在比较两个React节点树时,React首先比较根元素,随后的行为取决于根元素类型。

根节点不同类型

只要根节点类型不同,React就会销毁旧节点树,创建新节点树。

  • 在销毁节点树时,旧的DOM节点会被销毁,React组件实例触发componentWillUnmount()方法。

  • 在创建新节点树时,新的DOM节点被插入DOM树,React组件实例依次触发componentWillMount()方法和componentDidMount()方法。

根节点类型不同,根节点下任何组件或元素节点都会被卸载,并且把其state销毁,比如如下两棵节点树,diff后,卸载旧的Home组件,挂载一个新的Home组件实例:

节点相同类型

当比较两个相同类型的React节点(包括组件和html元素)时(包括所有节点,根节点和所有子节点),React比较两者的属性,不改变DOM节点,只更新当前节点需要改变的属性,如:

对于如上两个元素,React仅会改变当前节点需要改变的className属性。

样式更新

如果节点需要更新样式属性,React仅会更新当前节点需要改变的样式属性,如:

React仅会更新当前节点的font-size属性值。

React重复以上步骤,递归比较处理两棵React节点树.

组件节点相同类型

前面的diff比较都是以html元素为例,但是对于组件,前文的理论依然适用,只是关于组件的比较,我们还需要了解更多内容:当某一组件更新了,组件实例还没有改变,其组件状态是通过渲染维护的。React更新当前组件实例的(props)属性以匹配新元素,并且调用当前实例的componentWillReceiveProps()方法和componentWillUpdate()方法;然后调用render()方法并且使用diff算法递归比较已经更新的节点树和新返回的节点树。

递归比较子节点

默认地,在递归比较DOM节点的子节点时,React同时遍历两个子节点列表,当发现不同时,立即做出改变。
比如,需要在一个节点的子节点后插入一个子节点,性能是很好的,比较如下两棵节点树:

1
React匹配ul节点的子节点列表,依次比较<li>first</li>和<li>second</li>,直到发现当前元素缺少子节点<li>third</li>,就立即插入一个<li>third</li>节点。

但是如果不是在末尾插入新的子节点,而是在头部插入:

React比较第一个子节点时,就发现不同,立马改变该节点,随后全部子节点都需要改变,这样的性能表现很不好。

key属性

为了解决上面提到子节点比较的问题,React提供了key属性,如果某子节点拥有key属性,React会在原始子节点树里使用key值来比较该子节点,比如,对于如上实例,使用key属性:

对于如上这种结构,React会创建key值为3的节点,而只是移动key值为1和2的节点。

key属性值与唯一性

既然通过key值比较子节点,那么很明显,至少在子节点树里,每个key值必须是唯一的,但是并不需要保证key值是全局唯一的。

通常我们可以使用服务端返回的列表项数据中的id或index当key值。

diff算法的不足

我们必须明白,React diff算法决定如何重新渲染组件,但这只是一个细节实现,React可以对每一个action行为都重新渲染整个应用,最后的视图都是一样的,我们在不断地精炼探索式算法以提高应用性能。

目前的实现方式中,如前文的实例,我们知道在子节点间是可以不被重新渲染而只发生移动的,但是除此之外,子节点并不能被重用,算法将会重新渲染整个子节点树。

前文提到,React的diff算法是探索式的,基于两点假设,如果这两点假设不满足,性能就会降低: