写在前面
看到这个标题是不是有点激动?终于轮到这个哥中哥出场了。
众所周知,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: