virtualDom的DIFF算法关键过程整理

53次阅读

共计 4148 个字符,预计需要花费 11 分钟才能阅读完成。

判断对应节点是否有必要进行比较(sameVnode)
function sameVnode(oldVnode, vnode){
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}

如果值得比较会执行 patchVnode(oldVnode, vnode)

如果不值得比较,新节点直接把老节点整个替换了

打补丁(patchVnode)
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 种情况

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)
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)(后续称为比较区间)

(图片来自:https://github.com/aooy/blog/…)
通过对 oldStartVnode、oldEndVnode、newStartVnode、newEndVnode 做, 两两的 sameVnode 比较
比较判断有 4 种,按顺序依次比较 oldStartVnode —— newStartVnode(头对头)oldEndVnode —— newEndVnode(尾对尾)oldStartVnode —— newEndVnode(头对尾)oldEndVnode —— newStartVnode(尾对头)
如果其中一种比较成立了,那么 oldVode 中的相应节点会 以当前比较区间为基准 移到 newVnode 相应的位置上,然后比较区间会根据当前的比较条件类型,以头或尾为缩小比较区间的方向,缩小区间
例如:当 oldStartVnode,newEndVnode 值得比较时, 将 oldStartVnode.el 移动到 oldEndVnode.el 后边

当 4 种比较都不成立时,会使用 key 去比较,并在最终都使 newVode 的比较区间,头部 减 1
当 oldVnode 的 key 列表中能匹配到对应的 key 时,判断比较节点的选择器属性是否一样

不一样则直接在当前比较区间的头部, 新创建一个 newVnode 的 Dom 插入
比较节点中的 oldVnode 无需处理,因为后面的比较中不会有成立的比较条件,最终会直接删除节点

如果一样则将比较节点中的 oldVnode 移动到当前比较区间的头部(所以为节点设置 key 可以更高效的利用 dom),并将比较区间中 oldVnode 原本的索引位置赋值为 Null

如果最终 key 列表也没能匹配到的话,也是直接在当前比较区间的头部, 新创建一个 newVnode 的 Dom 插入。

最后在结束时会存在 2 种情况

oldStartIdx > oldEndIdx
oldVnode 先遍历完,证明 newVode 有新增的节点,或者一致,这种情况会将 newVode 剩余的节点插入到 oldVnode 比较区间的末尾

newStartIdx > newEndIdx
这时是 newVode 先遍历完,证明 newVode 里删除了某些节点,此时 oldVnode 的比较区间节点是已经不存在的,会将他们删除。

参考文章:解析 vue2.0 的 diff 算法

正文完
 0