VUE DIFF原理以及核心代码
VUE DIFF :
● 对于同层级节点首先对比新旧节点的头尾,头与头、尾与尾分别进行对比,寻找未移动的节点。
● 新旧节点头尾对比完后,然后进行头与尾、尾与头的交叉对比,这一步的目的是寻找可复用的节点。
● 在交叉对比结束后,因为有可能还有可复用的节点,所以创建一个老节点 keyToIndex 的哈希表 map 记录 key,然后继续遍历新节点索引通过 key 查找可以复用的旧的节点。
● 节点遍历完成后,通过新老索引,移除多余旧节点或者增加新节点。
核心代码
Vue2 的 diff 算法是一种用来比较新旧虚拟 DOM 的算法,它的目的是找出最少的 DOM 操作来更新视图。
Vue2 的 diff 算法的核心思想是 双端比较,也就是同时从新旧虚拟 DOM 的头部和尾部开始比较,通过四个指针来移动和匹配节点,从而减少不必要的遍历。
Vue2 的 diff 算法的核心代码如下:
// patchVnode 函数用来对比两个节点是否相同,如果相同则进行更新,如果不同则直接替换 function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 如果两个节点完全相同,直接返回 if (oldVnode === vnode) { return } // 获取旧节点的真实 DOM 元素 const elm = vnode.elm = oldVnode.elm // 如果两个节点都是静态的,且 key 相同,并且新节点已经被克隆或者有标记位 // 则可以认为两个节点相同,直接跳过 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { return } // 如果新节点是文本节点,直接更新文本内容 if (isUndef(vnode.text)) { if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 如果新节点不是文本节点,且有子节点 if (isDef(vnode.children)) { // 如果旧节点也有子节点,则对子节点进行 diff if (isDef(oldVnode.children)) { updateChildren(elm, oldVnode.children, vnode.children, insertedVnodeQueue, removeOnly) } else { // 如果旧节点没有子节点,则添加新节点的子节点到 DOM 中 addVnodes(elm, null, vnode.children, 0, vnode.children.length - 1, insertedVnodeQueue) } } else if (isDef(oldVnode.children)) { // 如果新节点没有子节点,但旧节点有子节点,则移除旧节点的子节点 removeVnodes(elm, oldVnode.children, 0, oldVnode.children.length - 1) } } else if (oldVnode.text !== vnode.text) { // 如果新旧节点都是文本节点,但内容不同,则更新文本内容 nodeOps.setTextContent(elm, vnode.text) } } // updateChildren 函数用来对比两个虚拟 DOM 数组,找出最小的 DOM 操作 function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 定义四个指针分别指向新旧数组的头部和尾部 let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] // 定义一个 map 对象用来存储旧数组中每个节点的 key 和索引的映射关系,方便查找 let oldKeyToIdx, idxInOld, vnodeToMove, refElm while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 跳过已经被处理过的空节点 if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 如果新旧数组的头部节点相同,就对比并更新这两个节点,然后头部指针后移 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 如果新旧数组的尾部节点相同,就对比并更新这两个节点,然后尾部指针前移 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 如果旧数组的头部节点和新数组的尾部节点相同,就对比并更新这两个节点,然后把旧节点移动到右边,然后头尾指针都移动 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 如果旧数组的尾部节点和新数组的头部节点相同,就对比并更新这两个节点,然后把旧节点移动到左边,然后头尾指针都移动 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 如果以上四种情况都不满足,就需要用 map 对象来查找新节点在旧数组中是否存在 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果不存在,说明是新创建的节点,就把它插入到旧头节点的前面 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } else { // 如果存在,就拿到对应的旧节点 vnodeToMove = oldCh[idxInOld] // 如果两个节点相同,就对比并更新这两个节点,然后把旧节点移动到旧头节点的前面,并把原来的位置置空 if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) oldCh[idxInOld] = undefined } else { // 如果两个节点不同,说明 key 相同但是元素不同,就创建一个新的元素,并插入到旧头节点的前面 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } } // 新数组的头指针后移 newStartVnode = newCh[++newStartIdx] } } // 如果旧数组先遍历完,说明有新的节点需要添加 if (oldStartIdx > oldEndIdx){ // 把新数组中剩余的节点添加到 DOM 中 addVnodes(parentElm, null, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 如果新数组先遍历完,说明有旧的节点需要移除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
代码总结
- 定义四个指针,分别指向新旧虚拟 DOM 数组的头部和尾部。
- 从两端开始比较,如果头部或尾部的节点相同,就对比并更新这两个节点,然后移动指针。
- 如果头尾交叉的节点相同,就对比并更新这两个节点,然后把旧节点移动到合适的位置,然后移动指针。
- 如果以上情况都不满足,就用一个 map 对象来存储旧数组中每个节点的 key 和索引的映射关系,然后根据新节点的 key 来查找旧节点是否存在。
- 如果存在,就对比并更新这两个节点,然后把旧节点移动到合适的位置,并把原来的位置置空。
- 如果不存在,就创建一个新的节点,并插入到合适的位置。
- 重复以上步骤,直到有一个数组遍历完。
- 如果旧数组先遍历完,说明有新的节点需要添加,就把新数组中剩余的节点添加到 DOM 中。
- 如果新数组先遍历完,说明有旧的节点需要移除,就把旧数组中剩余的节点从 DOM 中移除。
这样就可以实现最小化的 DOM 操作,提高渲染性能。