diff 算法详解
前言
Vue 中的 key 是用来做什么的?为什么不推荐使用 index 作为 key?
示例
假设有如下列表
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
它们的虚拟 dom 大概如下
{
tag: 'ul',
children: [
{tag: 'li', children: [ { vnode: { text: '1' } } ] },
{tag: 'li', children: [ { vnode: { text: '2' } } ] },
{tag: 'li', children: [ { vnode: { text: '3' } } ] },
]
}
假设更新后,我们把子节点的顺序调换一下
{
tag: 'ul',
children: [
{tag: 'li', children: [ { vnode: { text: '3' } } ] },
{tag: 'li', children: [ { vnode: { text: '2' } } ] },
{tag: 'li', children: [ { vnode: { text: '1' } } ] },
]
}
首先响应式数据更新后,触发了渲染watcher的回调函数vm._update(vm._render())去驱动视图更新
vm._render()其实就是生成的 vnode,vm._udpate就会带着新的 vnode 去触发__patch__过程
下面之间进入这个 patch 过程
不是相同节点
直接销毁旧的 vnode,渲染新的 vnode,这也解释了为什么 diff 是同层对比
是相同节点,要尽可能的做节点的复用(都是 ul,进入)
会调用src/core/vdom/patch.js下的patchVnode方法
- 如果新 vnode 是文字 vnode:直接调用浏览器的 api 把节点替换掉文字内容
- 如果新 vnode 不是文字 vnode
- 如果有新 children 而没有 children:说明是新增 children,直接 addVnodes 添加新子节点
- 如果有旧 children 而没有新 children:说明是删除 children,直接 removeVnodes 删除旧子节点
- 如果新旧 children 都存在(都存在 li 子节点列表,进入) 此时就是我们 diff 算法考察的核心点,也就是新旧节点的 diff 过程
// 旧首节点
let oldStartIdx = 0
// 新首节点
let newStartIdx = 0
// 旧尾节点
let oldEndIdx = oldCh.length - 1
// 新尾节点
let newEndIdx = newCh.length - 1
// 判断节点是否可用的函数,可以看到key是关键
function sameVnode(a, b) {
return a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)
}
diff 过程就是一个循环过程,每一轮都是统一的对比,其中某一项命中了,就递归进入 patchVnode 针对单个 vnode 进行的过程
- 旧首节点和新首节点用 sameNode 对比
- 旧尾节点和新首节点用 sameNode 对比
- 旧首节点和新尾节点用 sameNode 对比
- 旧尾节点和新尾节点用 sameNode 对比
- 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射表,然后用新 vnode 的 key 去找出在旧节点中可以复用的位置
然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇,说明这个端的节点都被 patch 过了。在指针相遇以后,还有两种比较特殊的情况:
- 有新节点要加如 如果更新完以后,
oldStartIdx > oldEndIdx,说明旧节点都被 patch 完了,但是有可能还有新的节点没有被处理到,接着回去判断是否要新增子节点 - 有旧节点要删除 如果新节点先 patch 完了,那么此时会走
newStartIdx > newEndIdx的逻辑,那么就会去删除多余的旧子节点
为什么不要用 index 作为 key
因为 key 的顺序没变,但是值却变了,这会导致一个问题:无法复用现有节点,从而导致多余的更新
解释: 按照最合理的逻辑来说,旧的第一个 vnode 应该是直接复用新的第三个 vnode
在子节点 diff 过程中,会在旧首节点和新首节点用 sameNode 对比,因为两者的 key 此时都是 0,所以会进入该逻辑,进行 patchVnode 操作,在 patchVnode 的时候检查到 props 改变了(原来是 1,现在变成了 3),然后会触发重渲染
为什么不要用随机数作为 key
随机数做 key 的话,会进入到 diff 过程最后一步,因为所有 key 都不相同,此时会进入这个流程:123 -> 先重新创建3个子组件 -> 321123 -> 删除、销毁后面3个子组件 -> 312