1 是什么
如果我现在修改了模板中的一个属性
那么 Vue 怎么更新渲染处理比较好呢?
- 把旧的 DOM 全部删除,然后渲染一个新的 DOM 替换上去?
这样显然成本比较高
为了节省开销,Vue 中会将新老虚拟DOM进行对比,尽量复用旧 DOM
这个过程就是 diff 算法
下面我们按照这个思路图,进行讲解

2 patch()
源码位置:vue-main\src\core\vdom\patch.ts
当数据更新,数据劫持,触发setter,派发更新,然后会执行 patch() 方法,会传入参数:老的虚拟节点、新的虚拟节点
首先,这里面会执行 sameVnode() ,判断当前两个节点是否是同类标签
- 不是同类标签:那就直接替换就完事了
- 是同类标签:就要进行进一步比较,看看他俩是不是完全一样的,调用 patchVnode() 方法
2.1 sameVnode() 是否是同类标签
sameVnode() 的作用是:通过对比 key、tag等等判断当前两个节点是否是同类标签
1 | function sameVnode(a, b) { |
3 patchVnode()
- 文本不同则覆盖文本
- 旧虚拟 DOM 多余子节点,则删除;
- 旧虚拟 DOM 少子节点,则新增
- 都有子节点,进行 updateChildren() 比较两个人的子节点
4 updateChildren() - diff 核心方法
4.1 双端交叉指针
vue2 中叫做双端交叉指针,新老Vdom各有两个指针,分别是:队头与队头,队尾与队尾、队头与队尾、队尾与队头。
while 循环中,存在几种比较:
1 | // while循环终止条件是指针越界 |
它会对比4次,如果说四次中寻找到的元素相同,就会去进行复用,移动元素的位置。
队头与队头比较:
- 指针指向:旧Vdom的队头、新Vdom的队头
- 调用
sameVnode()
方法,判断两个节点是否是同类标签- 是同类标签:调用
patchVnode()
对节点更新/递归比较(如替换旧DOM的文本/比较子节点);然后两个指针的index++
,本次 while 循环结束,进行下一轮循环 - 不是同类标签:进行队尾与队尾比较
- 是同类标签:调用

队尾与队尾比较:
- 指针指向:旧Vdom的队尾、新Vdom的队尾
- 调用
sameVnode()
方法,判断两个节点是否是同类标签- 是同类标签:调用
patchVnode()
对节点更新/递归比较(如替换旧DOM的文本/比较子节点);然后两个指针的index--
,本次 while 循环结束,进行下一轮循环 - 不是同类标签:进行队头与队尾比较
- 是同类标签:调用

队头与队尾比较:
- 指针指向:旧Vdom的队头、新Vdom的队尾
- 调用
sameVnode()
方法,判断两个节点是否是同类标签- 是同类标签:调用
patchVnode()
对节点更新/递归比较(如替换旧DOM的文本/比较子节点);然后两个指针的 index向内收缩,本次 while 循环结束,进行下一轮循环 - 不是同类标签:进行队尾与队头比较
- 是同类标签:调用

队尾与队头比较:
- 指针指向:旧Vdom的队尾、新Vdom的队头
- 调用
sameVnode()
方法,判断两个节点是否是同类标签- 是同类标签:调用
patchVnode()
对节点更新/递归比较(如替换旧DOM的文本/比较子节点);然后两个指针的 index向内收缩,本次 while 循环结束,进行下一轮循环 - 不是同类标签:进行第五种比较方法
- 是同类标签:调用
4.2 跳出while循环后
旧节点需要删除的情况
- 首先头头对比:两个节点
(文本A / key=1)
,判断为相同节点,复用,index++ - 下一轮循环,再进行头头对比,但此时
(文本XX / key=XX、文本B / key=2)
,不是相同节点,进行尾尾对比 - 尾尾对比:两个节点
(文本B / key=2)
,判断为相同节点,复用,index– - 下一轮循环,指针越界,跳出 while 循环
- 旧 Vnode 存在需要删除的节点,会在真实 DOM 中删除

旧节点需要添加的情况
如果旧节点没有,新节点有
就要把多余的节点就插入到老的

这里有两种情况:
- 最后是头头比较,要把多余的节点插入 old节点 后面
- 最后是尾尾比较:需要把多余的节点插入 old节点 前面
一个要插入前面、一个要插入后面,所以 Vue 中使用获取下一个元素方法来解决:
- 拿到这个多余节点的下一个元素,可能为空(头头比较情况),可能存在(尾尾比较情况)
- 执行 el.insertBefore(真实节点, 下一个元素)
- 如果下一个元素为空:就是头头比较的情况,则相当于appendChild() ,在老节点后面追加多余的节点
- 如果不为空:就是尾尾比较情况,在老节点中找到此下一个元素,要到会把多余节点插入到下一个元素的前面
4.3 如果 patch() 时发现 vnode 是组件
当 patch()
方法遇到组件 VNode 时,它会递归的执行:创建或更新组件实例,并进行相应的生命周期钩子函数的调用和组件的渲染更新。
因此如果我们使用函数式组件, patch()
则不会进行上面的过程,理论上会减少渲染开销
函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有
this
),也不会触发常规的组件生命周期钩子。
1 | <template functional> |
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.