diff 算法:找出虚拟dom(vnode)之间的差异,尽可能的复用节点。
当我们改变一个节点的时候,我们其实主要改了以下部分:
- 自身的属性(style 、class等等)
- 子节点
那么 diff 算法可以抽象为两部分:
- diff(找出差异)
- patch(打补丁更新dom)
举个栗子:
给定旧数组: [a,b,c,d],新数组: [e,f,g,h],找出新旧数组之间的差异。
我们约定以下名词 - 旧首(旧数组的第一个元素) - 旧尾(旧数组的最后一个元素) - 新首(新数组的第一个元素) - 新尾(新数组的最后一个元素)
一些工具函数:
sameVnode
用于判断节点是否应该复用,这里做了一些简化,实际的diff算法复杂些,这里只用tag 和 key 相同,我们就复用节点,执行patchVnode,即对节点进行修改。
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag
}
createKeyToOldIdx
建立key-index的索引,主要是替代遍历,提升性能
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
旧首 和 新首 对比
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode.elm, oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
旧尾 和 新尾 对比
if (sameVnode(oldEndVnode, newEndVnode)) { //旧尾 和 新尾相同
patchVnode(oldEndVnode.elm, oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
旧首 和 新尾 对比
if (sameVnode(oldStartVnode, newEndVnode)) {
//旧首 和 新尾相同,将旧首移动到 最后面
patchVnode(oldStartVnode.elm, oldStartVnode, newEndVnode);
nodeOps.insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
旧尾 和 新首 对比,将 旧尾 移动到 最前面
if (sameVnode(oldEndVnode, newStartVnode)) {
//旧尾 和 新首相同 ,将 旧尾 移动到 最前面
patchVnode(oldEndVnode.elm, oldEndVnode, newStartVnode);
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
首尾对比都不符合 sameVnode 的话,尝试用newCh的第一项在 oldCh 内寻找sameVnode,如果在 oldCh 不存在对应的 sameVnode ,则直接创建一个,存在的话则判断符合 sameVnode,则移动 oldCh 对应的节点,不符合 sameVnode ,创建新节点,最后 通过 oldStartIdx > oldEndIdx ,来判断 oldCh 和 newCh 哪一个先遍历完成
oldCh 先遍历完成,则证明 newCh 还有多余节点,需要新增这些节点;newCh 先遍历完成,则证明 oldCh 还有多余节点,需要删除这些节点。
总结:
1. diff 算法的核心是子节点数组对比,思路是通过首尾两端对比。
2. key的作用是:决定节点是否可以复用,建立key-index的索引,主要是替代遍历,提升性能。