在执行Diff算法的过程就是调用名为 patch 的函数,比较新旧节点。一边比较一边给真实的 DOM 打补丁。patch 函数接收两个参数 oldVnode 和 Vnode,它们分别代表新的节点和之前的旧节点。这个patch函数会比较 oldVnode 和 vnode 是否是相同的, 即函数 sameVnode(oldVnode, vnode), 根据这个函数的返回结果分如下两种情况:
这一步很重要其他几个点都很好理解,所以这里我们来详细来讲一下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)
}
}
首先介绍下这个函数中的变量定义:
(oldStartIdx = 0):oldVnode 的 startIdx, 初始值为 0
(newStartIdx = 0):vnode 的 startIdx, 初始值为 0
(oldEndIdx = oldCh.length - 1):oldVnode 的 endIdx, 初始值为 oldCh.length - 1
(oldStartVnode = oldCh[0]):oldVnode 的初始开始节点
(oldEndVnode = oldCh[oldEndIdx]):oldVnode 的初始结束节点
(newEndIdx = newCh.length - 1):vnode 的 endIdx, 初始值为 newCh.length - 1
(newStartVnode = newCh[0]):vnode 的初始开始节点
(newEndVnode = newCh[newEndIdx]):vnode 的初始结束节点
当 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 时,执行如下循环判断:
createKeyToOldIdx 的代码如下:
function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap {
let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch;
for (i = beginIdx; i <= endIdx; ++i) {
ch = children[i];
if (ch != null) {
key = ch.key;
if (key !== undefined) map[key] = i;
}
}
return map;
}
在createKeyToOldIdx 方法中,用 oldCh 中的 key 属性作为键,而对应的节点的索引作为值。然后再判断在 newStartVnode 的属性中是否有 key,且是否在 oldKeyToIndx 中找到对应的节点。
当上述 9 个判断执行完后,oldStartIdx 大于 oldEndIdx,则将 vnode 中多余的节点根据 newStartIdx 插入到 dom 中去;newStartIdx 大于 newEndIdx,则将 dom 中在区间 [oldStartIdx, oldEndIdx]的元素节点删除
到此 Diff 算法的执行过程结束!