【Vue2深度学习】虚拟DOM篇-Vue-Diff 算法

170 阅读5分钟

在《虚拟DOM篇-Vnode》中,我们介绍了VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM,然后对比新旧两份VNode,找出差异所在,再更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的。

而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程(在Vue中称作patch过程)。而这个过程中最关键的就是Vue-Diff算法,可以说,Vue-Diff算法是整个虚拟DOM的核心所在。

什么是Diff算法

diff算法是发生在虚拟DOM上的,新虚拟DOM和老虚拟DOM进行Diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。通过Diff算法,我们可以计算出虚拟DOM中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面,从而提升性能。

原始Diff算法

原始diff算法就是,两个虚拟DOM树,进行不分层级的逐一比对,也就是说,一个虚拟DOM树,从根节点到以后分支的每一个节点,都要单独拿出来跟新生成的节点做比较,这就是最原始的diff算法。

这个diff算法的时间复杂度表面上看是(n ^2),因为单独一个个的去跟另外的n个相比较,肯定是n ^2次就比较结束了,但是实际上不是的,比较完之后还要计算如何在最优的地方放置最佳的节点,所以就是O(n ^3)了。

虽然原始的Diff算法从功能上解决了先对比再处理实际DOM的需求,但是实际上我们的流程变得更加的复杂和笨拙。

img

优化Diff算法

优化Diff算法只比较同一层级 ,不做跨级比较。因为在实际的web展示中,非同级的节点移动是非常少的,所以可以选择做同级比较。

所谓同级比较,即只比较同层的节点,不同层不做比较。不同层的只需要删除原节点,并且新建插入更新节点。

img

Vue-Diff

Vue-Diff算法,采用的就是优化过的Diff算法,同层比较,不会跨级,且其比较是从从两侧向中间进行的,这种方式相对于从左到右依次比对的方式来说,更高效。

Vue-Diff策略
  • Tree Diff

    Tree Diff是对树每一层进行遍历,找出不同。

    img

  • Component Diff

    Vue是基于组件构建的,对于组件间的比较采用的策略如下:

    1. 如果是同一类型的组件,则按照原策略比较组件的虚拟 DOM 树,否则不需要比较。
    2. 如果是不同类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点

    img

    如上图,虽然组件 C 和组件 H 结构相似,但类型不同,Vue不会进行比较,会直接删除组件 C,创建组件 H。

  • Element Diff

    在进行组件对比的时候,如果两个组件类型相同,则需要进行元素级别的对比,这就是Element Diff。

    Element Diff时,提供了3种节点操作,分别为INSERT_MARKUP(插入),MOVE_EXISTING(移动),REMOVE_NODE(删除)。

    INSERT_MARKUP:新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。 MOVE_EXISTING:旧集合中有新组件类型,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点。

    REMOVE_NODE:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。

    浅析从Diff策略及源码角度剖析diff算法_子节点_04

Vue-Diff 过程
  • 准备

    定义4个指针:OldStartIdx、OldEndIdx、NewStartIdx、NewEndIdx

    img

  • 比较

    比较的是两个指针对应的节点的虚拟DOM是否为同一个,具体步骤如下:

    1. 比较OldStartIdx和NewStartIdx

      如果两个startIdx相同,则两个指针都会+1,也就是向后移一位,重新生成OldStartIdx和NewStartIdx指针。

      如果两个startIdx不一致,则比较两个endIdx

    2. 比较OldEndIdx和NewEndIdx

      如果两个endIdx一致,则两个endIdx都减1,也就是向前移一位,再执行步骤1。

    3. 如果两个startIdx和两个endIdx都不一致,则比较捺向的oldStartIdx和NewEndIdx

      如果oldStartIdx和NewEndIdx一致,则把oldStartIdx指向的虚拟DOM里的真实DOM节点,挪到OldEndIdx位置之后,oldStartIdx加1向后移一位,newEndIdx减1向前移动一位。

    4. 如果竖向和捺向都不一致,则比较撇向oldEndIdx和NewStartIdx。

      如果撇向一致,则把oldEndIdx指向的真实dom节点挪到oldStartIdx所在的真实dom前,同时oldEndIdx减1向前移动一位,newStartIdx加1向后移动一位。

    5. 如果竖向、捺向、撇向都不一致,则看有没有key。

      (1)如果有key,就能快速找到,并挪到oldStartIdx前。

      (2)如果没有key,就遍历oldStartIdx和oldEndIdx之间的所有节点,寻找newStartIdx指向的节点是否存在于这些老的vdom中。如果有,就把它挪到oldStartIdx前;没有就在oldStartIdx之前创建一个节点,newEndIdx减1向前移动一位。这样比较下去,一直到newEndIdx<newStartIdx。

  • 完成

    当newEndIdx<newStartIdx,这时new vnode生成完毕,然后将old vnode中多余的部分删掉即可,也就是oldStartIdx和oldEndIdx指向的dom及中间的部分。