react中diff算法

279 阅读4分钟

react中,render之后返回一个虚拟Dom树,通过对比前后两次render产生的虚拟Dom树,决定如何修改页面上真实Dom树,那么这个对比的过程使用的就是diff算法。

传统的diff算法首先递归比较两颗树的节点(O(n^2)),然后寻找最短的转换路径。最终达到的时间复杂度是O(n^3)。如果节点过多,那么性能消耗是巨大的。因此,react简单粗暴的修改了diff算法,将时间复杂度降到O(n)。不过该算法的修改是基于以下几个条件的:
  1. web ui 中跨层级的移动非常少,基本都是同层级的移动。

  2. 拥有相同类型的两个组件产生的DOM结构也是相似的,不同类型的两个组件产生的DOM结构则不近相同。
  3. 对于同层级的组件,通过唯一的key标记区分。

基于上述条件,react的diff算法采用同层级对比的方式,分别对tree diff、component diff 以及 element diff 进行算法优化。

tree diff

即对树进行分层比较,两棵树只会对同一层次的节点进行比较。如果一个父节点消失了,则不会继续对比其下的子节点,而是直接将该节点及其子节点会被完全删除掉,因此只需要一遍遍历。但是,跨层级的移动节点并不能享受react diff算法的优化。

component diff

react组件的对比策略:

1)如果不是同一类型组件,直接标记为dirty component,替换整个组件以及子组件。

2)如果是同一类型组件,比较state和props,记录下变化,再比较children,同样进行

component diff,记录下变化patch,之后再某个时间点统一更新。

3)对于同一类型的组件,有可能其Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP
(插入)、MOVE_EXISTING(移动)和REMOVE_NODE(删除)。
除了这三个,现在又增加了TEXT_CONTENT(文本内容改变)和SET_MARKUP(属性变化)。

element diff是整个diff算法的核心。

通过源码可以看到,nextChildren和prevChildren分别表示新的节点和旧的节点,他们以对象的形式存储ReactDOMComponent。

for in 循环遍历新集合nextChildren,通过key查找旧的集合中是否存在新的集合中的元素,如果如果不存在,prevChild即为undefined,如果存在相同节点,也即prevChild === nextChild,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较。这里比较lastIndex目的是为了优化不必要的移动。具体如下:遍历nextChildren过程中,当前节点在preChildren中的位置,如果大于旧的节点中已经访问过节点的最大位置lastIndex,则表示该节点在旧的节点中的位置本身就靠后,因此不需要移动,反之需要移动。

最后,还需要遍历一遍旧的节点,删去preChildren中存在,但是nextchildren中不存在的节点。

总而言之,采用的是先创建,再删除的步骤实现元素的更新、移动和删除。

补充:

key作为元素的标记,在涉及到数组的动态变更,例如数组新增元素、删除元素或者重新排序等,使用index作为key会导致展示错误的数据,例如:

代码块
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// 开始时:['a','b','c']=>
    <ul>
        <li key="0">a <input type="text"/></li>
        <li key="1">b <input type="text"/></li>
        <li key="2">c <input type="text"/></li>
    </ul>​
// 数组重排 -> ['c','b','a'] =>
    <ul>
        <li key="0">c <input type="text"/></li>
        <li key="1">b <input type="text"/></li>
        <li key="2">a <input type="text"/></li>
    </ul>
上面实例中在数组重新排序后,key对应的实例都没有销毁,而是重新更新。具体更新过程我们拿key=0的元素来说明, 数组重新排序后:组件重新render得到新的虚拟dom;新老两个虚拟

dom进行diff,新老版的都有key=0的组件,react认为同一个组件,则只可能更新组件;

然后比较其children,发现内容的文本内容不同(由a--->c),而input组件并没有变化,这时触发组件的componentWillReceiveProps方法,从而更新其子组件文本内容;
因为组件的children中input组件没有变化,其又与父组件传入的任props没有关联,所以input组件不会更新(即其componentWillReceiveProps方法不会被执行),导致用户输入的值不会变化。
注意:

同时应该避免使用Math.random()去作为key:key应该是稳定的,可预测的和独特的。不稳定的key(如由其生成的key Math.random())将导致许多组件实例和DOM节点被不必要地重新创建,这可能导致性能下降和子组件中的丢失状态。可以尝试使用一个全局变量保存当前的key,让key递增。