diff算法算是虚拟DOM的一个核心,也是一个难点.在这块我也是陆陆续续花费了两天才整明白并把这篇博客完成.方法主要是通过看snabbdom的updateChildren函数源码,在本文尾部也贴出来了.
diff 算法的核心是对比新旧节点的 children,更新 DOM
要对比两棵树的差异,可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,然后对比每一次的差异,这样的时间复杂度为 O(n^3)
,显然这样很低效.然而在DOM 操作时很少会把一个父节点移动/更新到某一个子节点,因此只需要找同级别子节点依次比较
,然后依次再找下一级别的节点比较,这样算法的时间复杂度为 O(n)
.所以diff算法的核心是对比新旧节点的childern,更新DOM.为了保证真实DOM中的顺序
和新节点保持一致可以用while循环
对首尾节点
进行对比,同时对新老节点数组的开始和结尾节点设置标记索引
,循环的过程中向中间移动索引
,这样既能实现排序
也能减小时间复杂度
.
Diff算法过程
(对比同级节点)
while循环开始
四种
比较方式(必须按顺序):
旧开始节点
与 新开始节点
比较,如果匹配上了(key 和 sel 相同),那么旧开始节点不动
.旧结束节点
与 新结束节点
比较,如果匹配上了(key 和 sel 相同),那么旧结束节点不动
.旧开始节点
与 新结束节点
比较,如果匹配上了(key 和 sel 相同),那么把旧开始节点移动到旧结束节点的后面
.(同时++oldStartIdx,–newEndIdx)开始下一次while循环.旧结束节点
与 新开始节点
比较,如果匹配上了(key 和 sel 相同),那么把旧结束节点移动到旧开始节点的前面
.新节点的 key
在老节点数组中找相同节点
移到旧开始节点的前面
.旧开始节点的前面
.创建新节点
并插入到旧开始节点的前面
.while循环结束
注: 这张图是我从网上copy下来的,用作diff算法的分析.如下:
需要注意的是,图上的标注在移动b的时候位置是错误的应该是在f的后面.
function updateChildren(parentElm: Node,oldCh: VNode[],newCh: VNode[],insertedVnodeQueue: VNodeQueue) {
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: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 索引变化后,可能会把节点设置为空
if (oldStartVnode == null) {
// 节点为空移动索引
oldStartVnode = oldCh[++oldStartIdx];
// Vnode might have been moved left
} 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)) {
// 1. 比较老开始节点和新的开始节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 2. 比较老结束节点和新的结束节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 3. 比较老开始节点和新的结束节点
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 4. 比较老结束节点和新的开始节点
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!,oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 开始节点和结束节点都不相同
// 使用 newStartNode 的 key 再老节点数组中找相同节点
// 先设置记录 key 和 index 的对象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,oldEndIdx);
}
// 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果是新的vnode
if (isUndef(idxInOld)) {
// New element
// 如果没找到,newStartNode 是新节点
// 创建元素插入 DOM 树
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
// 重新给 newStartVnode 赋值,指向下一个新节点
newStartVnode = newCh[++newStartIdx];
} else {
// 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新旧节点的选择器不同
// 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
} else {
// 如果相同,patchVnode()
// 把 elmToMove 对应的 DOM 元素,移动到左边
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, elmToMove.elm!,
oldStartVnode.elm!);
}
// 重新给 newStartVnode 赋值,指向下一个新节点
newStartVnode = newCh[++newStartIdx];
}
}
}
// 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 如果老节点数组先遍历完成,说明有新的节点剩余
// 把剩余的新节点都插入到右边
before = newCh[newEndIdx+1] == null ? null :newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,insertedVnodeQueue);
} else {
// 如果新节点数组先遍历完成,说明老节点有剩余
// 批量删除老节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}