简述 Vue2 的 Diff VNode 原理
说明:本文内容以 vue-2.6.14 的 dist/vue.js
为例
做 Diff VNode 的目的是尽量复用页面已存在的 Element,达到提高性能的目的。
方法的入口是 updateChildren
,该函数内会用到另外 4 个函数,分别是判断虚拟节点是否相同的 sameVnode
函数、将旧虚拟节点的页面元素赋给新虚拟节点的 patchVnode
函数、给虚拟节点创建页面元素的 addVnodes
函数和删除虚拟节点的 removeVnodes
函数。
updateChildren
方法接收 5 个参数,依次是 parentElm
、oldCh
、newCh
、insertedVnodeQueue
和 removeOnly
。
我们目前只需要关注 oldCh
和 newCh
,分别是旧虚拟节点队列和新虚拟节点队列,Diff VNode 比的就是这两个队列。下面具体说说对比的方法。
首先给两个队列的头尾各放一个指针,这样我们就会有 4 个指针,分别是旧队列的左指针 oldStartIdx 和右指针 oldEndIdx,它们指向的 VNode 分别是 oldStartVnode,oldEndVnode;新队列的左指针 newStartIdx 和右指针 newEndIdx,它们指向的 VNode 分别是 newStartVnode,newEndVnode,然后开始执行移动指针进行比对。
顺便说一下,以下说的“相同”是通过 sameVnode
函数判断的。
下面是指针移动规则:
- 如果
oldStartIdx
指向的 VNode 未定义,则oldStartIdx
右移一位,即oldStartIdx += 1
- 如果
oldEndIdx
指向的 VNode 未定义,则oldStartIdx
左移一位,即oldStartIdx -= 1
- 如果
oldStartVnode
和newStartVnode
相同,则两个左指针均右移一位 - 如果
oldEndtVnode
和newEndVnode
相同,则两个右指针均左移一位 - 如果
oldStartVnode
和newEndVnode
相同,则oldStartIdx
右移,newEndIdx
左移 - 如果
oldEndVnode
和newStartVnode
相同,则oldEndIdx
左移,newStartIdx
右移
以上 2 到 4 步会将两个相同虚拟节点传入 patchVnode
函数,将旧虚拟节点关联的页面元素赋给新虚拟节,即 vnode.elm = oldVnode.elm
。
以上步骤都没成功就会判断新虚拟节点是否在旧虚拟节点中出现过:
- 如果没出现过,则创建新的页面元素,并赋给
newStartVnode
- 如果出现过且相同,则
patchVnode
两个虚拟节点 - 否则同第一步
重复执行以上 6 + 3 共 9 个步骤,直到两个队列至少有一组指针相错,即 startIndex > endIndex,如果是 oldStartIndex > oldEndIndex,则说明新队列还有 ≥ 0 个 VNode 没处理,这时候调用 addVnodes
循环给多出来的 VNode 通过 createElm
创建元素;同样的,如果 newStartIndex > newEndIndex,则说明新队列比较短,通过 removeVnodes
循环删除旧队列里多余的元素。
以上是 Diff VNode 的大致思路,希望我说清楚了,但最好的办法还是自己动手画两个队列,然后模拟几种情况(新增、删除、插入等)。
PS:顺便吐槽下,我觉得 sameVnode
和 addVnodes
的方法名取的不是很好,前者用的 same 不是一个动词,对于方法或函数来说,最好是动词开头;后者给人的感觉是新增虚拟节点,其实只是新增页面元素关联给虚拟节点。