在《虚拟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的需求,但是实际上我们的流程变得更加的复杂和笨拙。
优化Diff算法
优化Diff算法只比较同一层级 ,不做跨级比较。因为在实际的web展示中,非同级的节点移动是非常少的,所以可以选择做同级比较。
所谓同级比较,即只比较同层的节点,不同层不做比较。不同层的只需要删除原节点,并且新建插入更新节点。
Vue-Diff
Vue-Diff算法,采用的就是优化过的Diff算法,同层比较,不会跨级,且其比较是从从两侧向中间进行的,这种方式相对于从左到右依次比对的方式来说,更高效。
Vue-Diff策略
-
Tree Diff
Tree Diff是对树每一层进行遍历,找出不同。
-
Component Diff
Vue是基于组件构建的,对于组件间的比较采用的策略如下:
- 如果是同一类型的组件,则按照原策略比较组件的虚拟 DOM 树,否则不需要比较。
- 如果是不同类型的组件,则将该组件判断为dirty component,从而替换整个组件下的所有子节点
如上图,虽然组件 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不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。
Vue-Diff 过程
-
准备
定义4个指针:OldStartIdx、OldEndIdx、NewStartIdx、NewEndIdx
-
比较
比较的是两个指针对应的节点的虚拟DOM是否为同一个,具体步骤如下:
-
比较OldStartIdx和NewStartIdx
如果两个startIdx相同,则两个指针都会+1,也就是向后移一位,重新生成OldStartIdx和NewStartIdx指针。
如果两个startIdx不一致,则比较两个endIdx
-
比较OldEndIdx和NewEndIdx
如果两个endIdx一致,则两个endIdx都减1,也就是向前移一位,再执行步骤1。
-
如果两个startIdx和两个endIdx都不一致,则比较捺向的oldStartIdx和NewEndIdx
如果oldStartIdx和NewEndIdx一致,则把oldStartIdx指向的虚拟DOM里的真实DOM节点,挪到OldEndIdx位置之后,oldStartIdx加1向后移一位,newEndIdx减1向前移动一位。
-
如果竖向和捺向都不一致,则比较撇向oldEndIdx和NewStartIdx。
如果撇向一致,则把oldEndIdx指向的真实dom节点挪到oldStartIdx所在的真实dom前,同时oldEndIdx减1向前移动一位,newStartIdx加1向后移动一位。
-
如果竖向、捺向、撇向都不一致,则看有没有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及中间的部分。