在执行Diff算法的过程就是调用名为 patch 的函数,比较新旧节点。一边比较一边给真实的 DOM 打补丁。patch 函数接收两个参数 oldVnode 和 Vnode,它们分别代表新的节点和之前的旧节点。这个patch函数会比较 oldVnode 和 vnode 是否是相同的, 即函数 sameVnode(oldVnode, vnode), 根据这个函数的返回结果分如下两种情况:
- true:则执行 patchVnode
- false:则用 vnode 替换 oldVnode
patchVnode 函数做的工作
- 找到对应的真实 dom,称为 el
- 判断 vnode 和 oldVnode 是否指向同一个对象。
- 如果是,那么直接 return。
- 如果他们都有文本节点并且不相等,那么将 el 的文本节点设置为 vnode 的文本节点。
- 如果 oldVnode 有子节点而 vnode 没有,则删除 el 的子节点。
- 如果 oldVnode 没有子节点而 vnode 有,则将 vnode 的子节点真实化之后添加到 el
- 如果两者都有子节点,则执行 updateChildren 函数比较子节点。
这一步很重要其他几个点都很好理解,所以这里我们来详细来讲一下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 时,执行如下循环判断:
- oldStartVnode 为 null,则 oldStartVnode 等于 oldCh 的下一个子节点,即 oldStartVnode 的下一个兄弟节点
- oldEndVnode 为 null, 则 oldEndVnode 等于 oldCh 的相对于 oldEndVnode 上一个子节点,即 oldEndVnode 的上一个兄弟节点
- newStartVnode 为 null,则 newStartVnode 等于 newCh 的下一个子节点,即 newStartVnode 的下一个兄弟节点
- newEndVnode 为 null, 则 newEndVnode 等于 newCh 的相对于 newEndVnode 上一个子节点,即 newEndVnode 的上一个兄弟节点
- oldEndVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldStartVnode, newStartVnode),执行完后 oldStartVnode 为此节点的下一个兄弟节点,newStartVnode 为此节点的下一个兄弟节点
- oldEndVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldEndVnode, newEndVnode),执行完后 oldEndVnode 为此节点的上一个兄弟节点,newEndVnode 为此节点的上一个兄弟节点
- oldStartVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldStartVnode, newEndVnode),执行完后 oldStartVnode 为此节点的下一个兄弟节点,newEndVnode 为此节点的上一个兄弟节点
- oldEndVnode 和 newStartVnode 为相同节点则执行 patchVnode(oldEndVnode, newStartVnode),执行完后 oldEndVnode 为此节点的上一个兄弟节点,newStartVnode 为此节点的下一个兄弟节点
- 使用 key 时的比较:
oldKeyToIdx为未定义时,由 key 生成 index 表,具体实现为 createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx),
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 中找到对应的节点。
- 如果不存在这个 key,那么就将这个 newStartVnode 作为新的节点创建且插入到原有的 root 的子节点中,然后将 newStartVnode 替换为此节点的下一个兄弟节点。
- 如果存在这个key,那么就取出 oldCh 中的存在这个 key 的 vnode,然后再进行 diff 的过程,并将 newStartVnode 替换为此节点的下一个兄弟节点。
当上述 9 个判断执行完后,oldStartIdx 大于 oldEndIdx,则将 vnode 中多余的节点根据 newStartIdx 插入到 dom 中去;newStartIdx 大于 newEndIdx,则将 dom 中在区间 [oldStartIdx, oldEndIdx]的元素节点删除
到此 Diff 算法的执行过程结束!