简单Diff算法对于DOM的移动操作并不是最优的
若使用
简单Diff算法,则会发生两次DOM操作,第一次DOM操作将真实DOM节点p-1移动到真实DOM节点p-3后面,第二次移动操作将真实DOM节点p-2移动到真实DOM节点p-1后面,上述过程并非最优解,在这个例子中,只需一步操作即可完成更新,把真实DOM节点p-3移动到p-1前面,本文介绍的双端Diff算法可以做到
双端比较原理
双端Diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法,因此需要四个索引值,分别指向新旧两组子节点的端点,有了索引后,就可以找到它所指向的虚拟节点
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children
const newChildren = n2.children
//四个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
//四个索引指向的vnode节点
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
}
在双端比较中,每一轮比较都分为四个步骤
理想情况
- 比较旧的一组子节点的第一个子节点p-1与新的一组子节点中的第一个子节点p-4,看是否相同,若key值相同则进行DOM复用,否则什么都不做
- 比较旧的一组子节点的最后一个子节点p-4与新的一组子节点中的最后一个子节点p-3,看是否相同,若key值相同则进行DOM复用,否则什么都不做
- 比较旧的一组子节点的第一个子节点p-1与新的一组子节点中的最后一个子节点p-3,看是否相同,若key值相同则进行DOM复用,否则什么都不做
- 比较旧的一组子节点的最后一个子节点p-4与新的一组子节点中的第一个子节点p-4,看是否相同,若key值相同则进行DOM复用,否则什么都不做
若第一步key值相同,可以复用,由于两者在新旧两组子节点中都是头部节点,因此不需要移动,只需调用patch函数进行打补丁,同时更新相关索引,指向下一个位置
若第二步key值相同,可以复用,由于两者都处于尾部,因此不需要对真实DOM进行移动操作,只需打补丁即可,同时更新索引到下一个位置
若第三步key值相同,可以复用,可以看出,p-1本身是头部节点,在新的顺序中,变为了尾部节点,所以要将尾部节点p-1对应的真实DOM移动到旧的一组子节点的尾部节点所对应的真实DOM后面,同时更新索引到下一个位置
若第四步key值相同,可以复用,p-4原本是最后一个子节点,在新的顺序中,变成了第一个子节点,将索引oldEndIdx所指向的真实DOM移动到索引oldStartIdx指向的虚拟节点所对应的真实DOM前面,同时更新索引到下一个位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVNode.key === newStartVNode.key) {
//打补丁,具体逻辑不赘述
patch(oldStartVNode, newStartVNode, container)
//更新索引指向下一个位置
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (oldEndVNode.key === newEndVNode.key) {
patch(oldEndVNode, newEndVNode, container)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key) {
patch(oldStartVNode, newEndVNode, container)
insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key) {
//调用patch函数进行打补丁
patch(oldEndVNode, newStartVNode, container)
//把oldEndVNode.el 移动到 oldStartVNode.el 前面
insert(oldEndVNode.el, container, oldStartVNode.el)
//移动DOM后,更新索引值,并指向下一个位置
oldEndVNode = oldChildren[--oldEndIdx]
newStartIdx = newChildren[++newStartIdx]
}
}
非理想情况
无法命中四个步骤中的任何一步
尝试看非头部,非尾部的节点能否复用,拿新的一组子节点中的头部节点去旧的一组子节点中寻找,并将该节点在旧的一组子节点中的索引存储到变量idInOld中,若找到可复用的节点,则将其对应的真实DOM移动到头部,并将其设置为undefined,同时更新newStartIdx到下一个位置,若找不到具有相同key值的节点,说明是新增节点,将其挂载到正确的位置
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVNode) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (!oldEndVNode) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (oldStartVNode.key === newStartVNode.key) {
patch(oldStartVNode, newStartVNode, container)
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (oldEndVNode.key === newEndVNode.key) {
patch(oldEndVNode, newEndVNode, container)
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key) {
patch(oldStartVNode, newEndVNode, container)
insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key) {
//调用patch函数进行打补丁
patch(oldEndVNode, newStartVNode, container)
//把oldEndVNode.el 移动到 oldStartVNode.el 前面
insert(oldEndVNode.el, container, oldStartVNode.el)
//移动DOM后,更新索引值,并指向下一个位置
oldEndVNode = oldChildren[--oldEndIdx]
newStartIdx = newChildren[++newStartIdx]
} else {
//当上述四个比较过程都无法找到可复用节点
const idxInOld = oldChildren.findIndex(
node => node.key == newStartVNode.key
)
//若能找到可复用节点,将其挪至尾部
if (idxInOld > 0) {
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove, newStartVNode, container)
insert(vnodeToMove.el, container, oldStartVNode.el)
oldChildren[idxInOld] = undefined
newStartVNode = newChildren[++newStartIdx]
} else {
//将newStartVNode作为新节点挂载到头部,并使用oldStartVNode.el作为锚点
patch(null, newStartVNode, container, oldStartVNode.el)
}
}
}
当一轮更新完毕后,若有节点在整个更新过程中被遗漏了,说明该节点只在旧节点中出现,在新节点中不存在,将其进行移除
if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
patch(null, newChildren[i], container, oldStartVNode.el)
}
} else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
//移除不存在元素
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
unmount(oldChildren[i])
}
}