Vue 的 diff 算法主要是用来比较新旧虚拟节点,找出差异并更新 DOM,核心思想是尽可能的复用节点,减少不必要的 DOM 操作。下面是 Vue diff 算法的代码实现:
- 首先是 patch 函数,它接收旧节点 oldVnode 和新节点 vnode,根据节点类型的不同,调用不同的 patch 函数:
function patch (oldVnode, vnode) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el const parentEle = api.parentNode(oEl) createEle(vnode, function () { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oEl) oldVnode = null }) } return vnode }
sameVnode
函数用于判断两个节点是否相同,它需要满足以下条件:key 相同、标签名相同、命名空间相同、同为注释节点或者同为具有相同 data 属性的文本节点
function sameVnode (a, b) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && !!a.data === !!b.data && sameInputType(a, b) ) } function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = (i = a.data) && (i = i.attrs) && i.type const typeB = (i = b.data) && (i = i.attrs) && i.type return typeA === typeB }
patchVnode
函数用于对比两个同类型节点的差异,这里分为四种情况处理:新节点为文本节点、旧节点为真实节点、新旧节点均为具有子节点的节点、新旧节点均为无子节点的节点。
function patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el let i, oldCh = oldVnode.children, ch = vnode.children if (vnode.text !== null) { api.setTextContent(el, vnode.text) } else { if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) } else if (ch) { if (oldVnode.text) api.setTextContent(el, '') addVnodes(el, null, ch, 0, ch.length - 1) } else if (oldCh) { removeVnodes(el, oldCh, 0, oldCh.length - 1) } } }
updateChildren
函数用于对比两个具有子节点的节点,它会通过sameVnode
函数找到旧节点和新节点中相同的节点,然后通过递归调用patch
函数进行更新。
function updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0, newStartIdx = 0 let oldEndIdx = oldCh.length - 1, newEndIdx = newCh.length - 1 let oldStartVnode = oldCh[0], newStartVnode = newCh[0] let oldEndVnode = oldCh[oldEndIdx], newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartVnode) { oldStartVnode = oldCh[++oldStartIdx] } else if (!oldEndVnode) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx]