function sameVnode(oldVnode, vnode){ return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel }
patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) }else if (ch){ createEle(vnode) //create el's children dom }else if (oldCh){ api.removeChildren(el) } } }
节点的比较有5种状况vue
if (oldVnode === vnode)
,他们的引用一致,能够认为没有变化。if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text)
,文本节点的比较,须要修改,则会调用Node.textContent = vnode.text。if( oldCh && ch && oldCh !== ch )
, 两个节点都有子节点,并且它们不同,这样咱们会调用updateChildren函数比较子节点,这是diff的核心。else if (ch)
,只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。else if (oldCh)
,新节点没有子节点,老节点有子节点,直接删除老节点。updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0, 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] let oldKeyToIdx let idxInOld let elmToMove let before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null oldStartVnode = oldCh[++oldStartIdx] }else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx] }else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] }else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx] }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] }else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else { // 使用key时的比较 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表 } idxInOld = oldKeyToIdx[newStartVnode.key] if (!idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) }else { patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] = null api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) } newStartVnode = newCh[++newStartIdx] } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) }else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
主要的思路大概就是,定义变量,分别记录当前比较的新、旧节点中的首尾索引与节点(oldStartVnode、oldEndVnode、newStartVnode、newEndVnode)(后续称为比较区间)node
(图片来自:https://github.com/aooy/blog/... )git
经过对 oldStartVnode、oldEndVnode、newStartVnode、newEndVnode 作,两两的 sameVnode 比较github
比较判断有4种,按顺序依次比较
oldStartVnode —— newStartVnode (头对头)
oldEndVnode —— newEndVnode (尾对尾)
oldStartVnode —— newEndVnode (头对尾)
oldEndVnode —— newStartVnode (尾对头)
若是其中一种比较成立了,那么oldVode中的相应节点会 以当前比较区间为基准 移到newVnode相应的位置上,
而后比较区间会根据当前的比较条件类型,以头或尾为缩小比较区间的方向,缩小区间算法
例如: 当oldStartVnode,newEndVnode值得比较时, 将oldStartVnode.el移动到oldEndVnode.el后边
当4种比较都不成立时,会使用key去比较,并在最终都使newVode的比较区间,头部 减1
api
当oldVnode的key列表中能匹配到对应的key时,判断比较节点的选择器属性是否同样dom
不同则直接在当前比较区间的头部,新建立
一个newVnode的Dom插入函数
比较节点中的oldVnode无需处理,由于后面的比较中不会有成立的比较条件,最终会直接删除节点
若是最终key列表也没能匹配到的话,也是直接在当前比较区间的头部,新建立
一个newVnode的Dom插入。spa
最后在结束时会存在2种状况code
oldStartIdx > oldEndIdx
oldVnode先遍历完,证实newVode有新增的节点,或者一致,这种状况会将newVode剩余的节点插入到oldVnode比较区间的末尾
newStartIdx > newEndIdx
这时是newVode先遍历完,证实newVode里删除了某些节点,此时oldVnode的比较区间节点是已经不存在的,会将他们删除。
参考文章: 解析vue2.0的diff算法