July 12, 2023

Vue2源码(4):diff 算法原理

1 是什么

如果我现在修改了模板中的一个属性

那么 Vue 怎么更新渲染处理比较好呢?

这样显然成本比较高

为了节省开销,Vue 中会将新老虚拟DOM进行对比,尽量复用旧 DOM

这个过程就是 diff 算法

下面我们按照这个思路图,进行讲解

2 patch()

源码位置:vue-main\src\core\vdom\patch.ts

当数据更新,数据劫持,触发setter,派发更新,然后会执行 patch() 方法,会传入参数:老的虚拟节点、新的虚拟节点

首先,这里面会执行 sameVnode() ,判断当前两个节点是否是同类标签

2.1 sameVnode() 是否是同类标签

sameVnode() 的作用是:通过对比 key、tag等等判断当前两个节点是否是同类标签

1
2
3
4
5
6
7
8
9
10
11
function sameVnode(a, b) {
return (
a.key === b.key && // key值是否一样
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag && // 标签名是否一样
a.isComment === b.isComment && // 是否都为注释节点
isDef(a.data) === isDef(b.data) && // 是否都定义了data
sameInputType(a, b)) || // 当标签为input时,type必须是否相同
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}

3 patchVnode()

4 updateChildren() - diff 核心方法

4.1 双端交叉指针

vue2 中叫做双端交叉指针,新老Vdom各有两个指针,分别是:队头与队头,队尾与队尾、队头与队尾、队尾与队头。

while 循环中,存在几种比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// while循环终止条件是指针越界
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
// 队头与队头
patchVnode()
}else if(sameVnode(oldEndVnode, newEndVnode)){
// 队尾与队尾
patchVnode()
}else if(sameVnode(oldStartVnode, newEndVnode)){
// 队头与队尾
patchVnode()
}else if(sameVnode(oldEndVnode, newStartVnode)){
// 队尾与队头
patchVnode()
}else{
// 这四种情况都没有匹配到,进行第五种比较方法
// 如果这四种情况都没有匹配到,就会从新Vdom的队头开始,在旧Vdom开始寻找,找到了就该节点移动到对应位置,并将旧Vdom中该节点设置为undefined,如果没 // 找到,就创建新节点插入
}
}

它会对比4次,如果说四次中寻找到的元素相同,就会去进行复用,移动元素的位置。

队头与队头比较:

队尾与队尾比较:

队头与队尾比较:

队尾与队头比较:

4.2 跳出while循环后

旧节点需要删除的情况

旧节点需要添加的情况

如果旧节点没有,新节点有

就要把多余的节点就插入到老的

这里有两种情况:

一个要插入前面、一个要插入后面,所以 Vue 中使用获取下一个元素方法来解决:

4.3 如果 patch() 时发现 vnode 是组件

patch() 方法遇到组件 VNode 时,它会递归的执行:创建或更新组件实例,并进行相应的生命周期钩子函数的调用和组件的渲染更新。

因此如果我们使用函数式组件, patch() 则不会进行上面的过程,理论上会减少渲染开销

函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。

1
2
3
4
5
<template functional>
<div>
.......
</div>
</template>

5 面试题

5.1 面试题:为什么v-for循环时,key不能使用索引?

如下图,如果当前有一个数组存储:[‘苹果’, ‘梨’, ‘香蕉’]

使用 v-for 循环,渲染到页面上

并将苹果置为被勾选状态

如果现在使用 索引 作为 key

用户向数组中 unshift 一个火龙果

那么界面,就变成了火龙果被勾选的状态

这是为什么??

5.2 原因

因为修改数据后,虚拟DOM转换为真实DOM过程的 diff 算法

sameVnode() 采用 key、tag、inputType 判断是否是同类标签

之前苹果的索引为0

插入火龙果后,火龙果的索引依然是0

此时 sameVnode() 判断两个节点是同类标签,进一步比较 patchVnode(),仅使用新文本替换旧文本

勾选状态还在

所以火龙果就被勾选了

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.