详解Vue的Diff算法

284 阅读3分钟

写在前面

看到这个标题是不是有点激动?终于轮到这个哥中哥出场了。
众所周知,Vue 的 Diff算法 灵感来源于 snabbdom,snabbdom 这个单词来源于瑞典语,意思为速度。这说明了两个问题,1.作者是个瑞典籍人;2.作者对自己的这个项目很有信心。
好,接下来我们即将走入这个算法内部,探索怎么快,和 Vue 做了哪些改进。
我们先来想一下,snabbdom 中是运用 patch / h 来进行 虚拟DOM 的比对和生成的,那么 Vue 中怎么做才能使用类似的写法达到效果呢?
当然,你可以使用 vm.options.data 内的数据通过依赖触发达到更新节点的效果,不过我给出一种独家奇巧淫技,不走响应式,直接挂载元素:

/*
<div id="app"></div>
<button id="btn">click me</button>
*/
const vm = new Vue({
  el: '#app'
})
let vnode = null
const patch = vm.__patch__
const h = vm.$createElement
vnode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D')
])
const oldVnode = patch(vm._vnode, vnode)
document.querySelector('#btn').onclick = () => {
  vnode = h('ul', [
    h('li', { key: 'E' }, 'E'),
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D')
  ])
  patch(oldVnode, vnode)
}

为什么我会想到这种方式?因为 snabbdom 就是这么调用的。我们这章重点分析的是 Diff算法,因此这样的写法就足够了。
好了,自此开始,让我们进入 Diff算法 的领域吧~

四项命中原则

什么叫 Diff算法?四项命中原则!我不想和别人一样循序渐进,要来就先来最核心的。
先记下口诀: 新前旧前 | 新后旧后 | 新后旧前 | 新前旧后

前和后

旧节点的 children 里的第一个元素叫做旧前,最后一个元素叫做旧后
新节点的 children 里的第一个元素叫做新前,最后一个元素叫做新后

  • 设置循环体 -> 旧前<=旧后 && 新前<=新后
  • 按上面口诀的顺序,通过判断当前对应的新旧节点是否是同一节点来确认当前策略是否命中。
  • 若命中,则 x前 的指针下移,x后 的指针上移。

例1 - 新前旧前

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 新前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'E' }, 'E'), // 新后
])

判断 -> 新前旧前 -> 发现都是 key 为 A 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'), // 旧前
  h('li', { key: 'C' }, 'C'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'), // 新前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'E' }, 'E'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 B 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后 + 旧前
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 新前
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'E' }, 'E'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后
                              // 旧前
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 新前
  h('li', { key: 'E' }, 'E'), // 新后
])

循环 -> 跳出 -> ∵ 旧前超过了旧后的位置
这时候看新节点的 children,新前和新后之间还有两个元素,说明这两个是新增的 -> 将这两个追加到旧后的后面

例2 - 新前旧前 + 新后旧后

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 新前
  h('li', { key: 'C' }, 'C'), // 新后
])

判断 -> 新前旧前 -> 发现都是 key 为 A 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'), // 旧前
  h('li', { key: 'C' }, 'C'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'C' }, 'C'), // 新后 + 新前
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧后上移一位 -> 新后上移一位

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'), // 旧前 + 旧后
  h('li', { key: 'C' }, 'C'),
])
const newVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 新后
  h('li', { key: 'C' }, 'C'), // 新前
])

循环 -> 跳出 -> ∵ 新前超过了新后的位置
这时候看旧节点的 children,旧前和旧后之间还有一个元素,说明这个是多余的 -> 直接删除

例3 - 新前旧前 + 新后旧后 + 新后旧前

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'), // 新前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'A' }, 'A'), // 新后
])

判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现都是 key 为 A 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新后上移一位 -> 将该元素移到旧后之后,并将原位置设置为 undefined

const oldVNode = h('ul', [
  undefined,
  h('li', { key: 'B' }, 'B'), // 旧前
  h('li', { key: 'C' }, 'C'), // 旧后
  h('li', { key: 'A' }, 'A'), // -> insert
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'), // 新前
  h('li', { key: 'C' }, 'C'), // 新后
  h('li', { key: 'A' }, 'A'),
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 B 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  undefined,
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后 + 旧前
  h('li', { key: 'A' }, 'A'), // -> insert
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 新后 + 新前
  h('li', { key: 'A' }, 'A'),
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  undefined,
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 旧后
  h('li', { key: 'A' }, 'A'), // -> insert + 旧前
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 新后
  h('li', { key: 'A' }, 'A'), // 新前
])

循环 -> 跳出 -> ∵ 旧前超过了旧后的位置
这时候看新节点的 children,但是新前也超过了新后的位置,说明 Diff 完成 -> 删除 undefined 项 -> 不需要做任何事

例4 - 新前旧前 + 新后旧后 + 新后旧前 + 新前旧后 + 旧节点中不存在的新元素

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'B' }, 'B'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'), // 新前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 新后
])

判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现都是 key 为 B 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧后上移一位 -> 新前下移一位 -> 将该元素移到旧前之前,并将原位置设置为 undefined

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'C' }, 'C'), // 旧后
  undefined,
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'), // 新前
  h('li', { key: 'D' }, 'D'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧后上移一位 -> 新前下移一位 -> 将该元素移到旧前之前,并将原位置设置为 undefined

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'A' }, 'A'), // 旧前 + 旧后
  undefined,
  undefined,
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 新后 + 新前
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现不是同一个 -> 未命中
判断 -> 在旧节点中寻找相同 key 的元素 -> 不存在
移动 -> 新增并将该元素追加到旧前之前 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // -> insert
  h('li', { key: 'A' }, 'A'), // 旧前 + 旧后
  undefined,
  undefined,
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 新后
                              // 新前
])

循环 -> 跳出 -> ∵ 新前超过了新后的位置
这时候看旧节点的 children,旧前和旧后之间还有一个元素,说明这个是多余的 -> 直接删除 -> 删除 undefined 项

例5 - 新前旧前 + 新后旧后 + 新后旧前 + 新前旧后 + 旧节点中存在完全相同,但没命中的元素

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'), // 新前
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'), // 新后
])

判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现不是同一个 -> 未命中
判断 -> 在旧节点中寻找相同 key 的元素 -> 存在
判断 -> 旧节点中找到的元素和新前是不是同一个元素 -> 是 -> 调用 patchVnode
移动 -> 将该元素追加到旧前之前,并将原位置设置为 undefined -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'), // 旧前
  undefined,
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'), // 新前
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 A 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  undefined,                  // 旧前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新前
  h('li', { key: 'C' }, 'C'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现旧前是 undefined
移动 -> 旧前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  undefined,                
  h('li', { key: 'C' }, 'C'), // 旧前
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新前
  h('li', { key: 'C' }, 'C'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新后上移一位 -> 将该元素移到旧后之后,并将原位置设置为 undefined

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  undefined,                
  undefined,
  h('li', { key: 'D' }, 'D'), // 旧后 + 旧前
  h('li', { key: 'C' }, 'C'),
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新前 + 新后
  h('li', { key: 'C' }, 'C'),
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 D 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前上移一位 -> 新前上移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  undefined,                
  undefined,
  h('li', { key: 'D' }, 'D'), // 旧后
  h('li', { key: 'C' }, 'C'), // 旧前
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新后
  h('li', { key: 'C' }, 'C'), // 新前
])

循环 -> 跳出 -> ∵ 旧前超过了旧后的位置
这时候看新节点的 children,但是新前也超过了新后的位置,说明 Diff 完成 -> 删除 undefined 项 -> 不需要做任何事

例6 - 新前旧前 + 新后旧后 + 新后旧前 + 新前旧后 + 旧节点中存在 key 相同子元素却不同,而且没命中的元素

const oldVNode = h('ul', [
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'), // 新前
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'), // 新后
])

判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现不是同一个 -> 未命中
判断 -> 在旧节点中寻找相同 key 的元素 -> 存在
判断 -> 旧节点中找到的元素和新前是不是同一个元素 -> 不是 -> 新增该元素
移动 -> 将该元素追加到旧前之前 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'), // 旧前
  h('li', { key: 'B' }, 'B'),
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'), // 新前
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现都是 key 为 A 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧前下移一位 -> 新前下移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'B' }, 'B'), // 旧前
  h('li', { key: 'C' }, 'C'),
  h('li', { key: 'D' }, 'D'), // 旧后
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新前
  h('li', { key: 'C' }, 'C'), // 新后
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现不是同一个 -> 未命中
判断 -> 新后旧前 -> 发现不是同一个 -> 未命中
判断 -> 新前旧后 -> 发现都是 key 为 D 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧后上移一位 -> 新前下移一位 -> 将该元素移到旧前之前,并将原位置设置为 undefined

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'B' }, 'B'), // 旧前
  h('li', { key: 'C' }, 'C'), // 旧后
  undefined,
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'C' }, 'C'), // 新后 + 新前
])

循环 -> 继续
判断 -> 新前旧前 -> 发现不是同一个 -> 未命中
判断 -> 新后旧后 -> 发现都是 key 为 C 的 li 标签 -> 命中 -> 调用 patchVnode
移动 -> 旧后上移一位 -> 新后上移一位

const oldVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'),
  h('li', { key: 'B' }, 'B'), // 旧前 + 旧后
  h('li', { key: 'C' }, 'C'),
  undefined,
])
const newVNode = h('ul', [
  h('li', { key: 'B' }, 'Fake B'),
  h('li', { key: 'A' }, 'A'),
  h('li', { key: 'D' }, 'D'), // 新后
  h('li', { key: 'C' }, 'C'), // 新前
])

循环 -> 跳出 -> ∵ 新前超过了新后的位置
这时候看旧节点的 children,旧前和旧后之间还有一个元素,说明这个是多余的 -> 直接删除 -> 删除 undefined 项

简单记忆

  • 新前旧前新后旧后命中就 patchVnode,然后 x前下移一位,x后上移一位
  • 新后旧前命中,新后上移一位,旧前下移一位,将该元素插入旧后之后,并将原位置设置为 undefined
  • 新前旧后命中,新前下移一位,旧后上移一位,将该元素插入旧前之前,并将原位置设置为 undefined
  • 都没命中就去旧节点的 children 中找:
    • 找到了完全相同的元素说明只是位置改变,将该元素插入旧前之前,并将原位置设置为 undefined,然后将新前下移一位
    • 找到了 key 相同但 children / text 不同的元素,那就以新增的来对待,那就创建新元素放到旧前之前并将新前下移一位
    • 找不到说明也是新增的,那就创建新元素并插入旧前之前,然后将新前下移一位
  • 如果新节点先遍历完就去看旧节点的 children,旧前旧后之间的全部删除
  • 如果旧节点先遍历完就去看新节点的 children,新前新后之间的全部创建为新元素并插入旧前之前

疑问

我相信通过这6个案例大家应该就明白四项命中原则的原理了,如果还不理解,建议多看几遍,或者画个图自己感受一下指针的变化。
此外,我有失偏颇地认为大家应该不会太记得什么时候将元素移动到旧前之前或是旧后之后。我们可以想一下,当一个新节点中有一个子节点竟然匹配到了在旧节点中名列前茅的子节点,那不就说明这个旧节点被移动到底下去了吗?这时候就应该用旧后之后,其余的时候都是使用旧前之前
当然,我在其中还提前写了个调用 —— patchVnode,不用急,这是下一节的主角,且听我慢慢道来。

patchVnode

在上面例5例6中会发现,当四项基本原则都没命中时会进入旧节点树中找相同 key 的子元素。但是在找到后还要去比对一下这俩是不是只有 key 相同,其余的不同。这一步在这里被需要,在 patchVnode 中同样也需要。
首先我们需要知道一点,一个节点要么是元素节点要么是文本节点。说明在 VNode 对象中 children 和 text 这俩属性只会存在一个。
那我们先来简单思考一下,如果一个节点发生变化的话会出现几种情况:

└─ patchVnode
   ├─ 新节点不是文本节点 ( 不存在 text 属性 )
   │  ├─ 新旧节点都是元素节点 ( 存在 children 属性 )
   │  ├─ 只有新节点是元素节点 ( 存在 children 属性 )
   │  └─ 只有旧节点是元素节点 ( 存在 children 属性 )
   │
   └─ 新节点是文本节点 ( 存在 text 属性 )
  • 如果新节点是文本节点的话,那不管旧节点是什么类型的节点,直接设置旧节点的 children 为 undefined,然后让旧节点的 text 改为 新节点的 text 完事儿
  • 如果新节点不是文本节点的话
    • 如果新旧节点都是元素节点,执行 Diff算法,进行精细化比较
    • 如果只有新节点是元素节点,清空旧节点的 text 属性,并将新节点的子节点追加到旧节点中,并更新 DOM树
    • 如果只有旧节点是元素节点,说明旧节点的子节点都被删除了,那就清空旧节点的 children 属性,并更新 DOM树

patch

const modules = platformModules.concat(baseModules)
const patch = createPatchFunction({ nodeOps, modules })
Vue.prototype.__patch__ = inBrowser ? patch : noop

由这段代码我们首先要认清一件事儿,那就是 _patch_ 方法是一个高阶函数 createPatchFunction 内部所返回的方法,这就意味着虽然我们只是看起来简简单单地调用了 _patch_,但其实在这之前它做了很多我们看不见的工作。
其中最重要的就是上面代码中这段 const modules = platformModules.concat(baseModules),从代码层面上我们很容易就能明白,目的是为了将 web平台 提供的 modules 和 我们源码核心层面提供的 modules 进行合并。那这两个 modules 分别是什么呢?
如果你曾看过 一口气看完Vue源码 一文你就会知道,它是一个储存了共19个钩子的二维数组,第二维的数组中存放了钩子的方法。为了省事我直接引用之前的内容稍作修改:

└─ 调用 createPatchFunction 方法时会遍历 modules 初始化 cbs
   ├─ 源码核心层面提供的 baseModules 中分别提供了如下的钩子:
   │  ├─ directives.js 中集成了对于 directive 的 **create: updateDirectives / update: updateDirectives / destroy: unbindDirectives** 钩子
   │  └─ ref.js 中集成了对于 $refs 的 **create / update / destroy** 钩子
   │
   ├─ 此外在web平台提供的 platformModules 中分别提供了如下的钩子:
   │  ├─ attrs.js 中集成了对于 虚拟DOM 中 attrs 的 **create: updateAttrs / update: updateAttrs** 钩子 ( 即标签中的 :src 等 )
   │  ├─ class.js 中集成了对于 标签class 的 **create: updateClass / update: updateClass** 钩子 ( 即标签中的 class 和 :class )
   │  ├─ dom-props.js 中集成了对于 虚拟DOM 中 domProps 的 **create: updateDOMProps / update: updateDOMProps** 钩子 ( 即直接给子组件标签行内添加属性 )
   │  ├─ events.js 中集成了对于 虚拟DOM 中 on 的 **create: updateDOMListeners / update: updateDOMListeners** 钩子
   │  ├─ style.js 中集成了对于 虚拟DOM 中 staticStyle 和 style 的 **create: updateStyle / update: updateStyle** 钩子 ( 即标签中的 :style )
   │  └─ transition.js 中集成了对于 Vue 官方提供的 **过渡** 组件的 **create: _enter / activate: _enter / remove** 钩子
   │
   └─ 由此可知 modules 中一共有19个钩子,将 modules 中的钩子以相同的属性名划分到同一个数组中,然后分别存入 cbs 中,即:
     ├─ create: [updateAttrs, updateClass, updateDOMListeners, updateDOMProps, updateStyle, _enter, create, updateDirectives, ]
     ├─ activate: [_enter, ]
     ├─ update: [updateAttrs, updateClass, updateDOMListeners, updateDOMProps, updateStyle, update, updateDirectives, ]
     ├─ remove: [remove, ]
     └─ destroy: [destroy, unbindDirectives, ]

好了,进入正题。虽然上面讲了那么多,但是这个方法才是 Diff算法 的入口方法。

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  let isInitialPatch = false
  const insertedVnodeQueue = []
  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue, parentElm, refElm)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode)
      }
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

在这个方法中我们可以看到它一共做了2次大的判断:

  • 判断新节点是否存在
  • 判断旧节点是否存在 如果新节点不存在,那直接调用销毁钩子就完事了,还要比较啥呢对吧。
    如果旧节点不存在,说明这是一次空挂载,创建一个根节点。
    如果旧节点存在,还要进一步判断旧节点:
  • 如果是 虚拟DOM 而且新旧节点是同一个节点,那就调用 patchVnode 方法
  • 否则管它是不是 虚拟DOM,新节点直接插入到旧节点之前,然后把旧节点删除

疑问

那我们上面讲的这19个钩子有啥用?
有用,当然有用,删除旧节点的时候要不要调用 destroy 和 remove 钩子?更新旧节点的时候要不要调用 update 钩子?创建新节点的时候要不要调用 create 钩子?

写在最后

虽然本篇是写完了 Diff算法 相关的内容,但其实还有些细节没有讲到,比如 patchVnode 方法里 if (isDef(i = data.hook) && isDef(i = i.update))if (isDef(i = data.hook) && isDef(i = i.postpatch)) 中的 data.hook.(update | postpatch) 都哪儿来的?
这其实都来自于 vm._c 和 vm.$createElement 形式的 h函数 在生成 虚拟DOM 的过程中所调用的 createComponent 方法里所定义的。这部分的内容其实也不少,但并不影响我们对 Diff算法 本身的理解,等我有机会再写成文章分享出来。