浅析vue2的虚拟dom与diff算法
2024-12-09 11:13:16

这个也是较为常考的面试八股题,很早之前面试准备时候了解过,如今也算是捡起来重新看看了。

正文

本篇算是Vue响应式原理浅析那篇的补充,在响应式原理中,着重讲了vue实现响应式原理的核心思路,但是在针对页面渲染这块没怎么细说。

虚拟dom渲染这块,面试核心便是算法的思路,真让你手写实现,大概率也最多就是个伪代码的思路图,不可能真让大家去实现的。

所以,在本篇,本文并不深入探讨算法的源码实现,更多的是讲明白这个渲染的思路。

虚拟DOM

虚拟dom在响应式原理那边解释过,如果直接操作dom进行响应式的修改的话,对页面的性能开销会很大。

作为前端开发,我们都知道的,重排是比重绘性能开销大的多的操作,如果每个变动都要直接操作整个DOM树进行替换,那么毫无疑问,vue绝对会被扫入历史垃圾堆。

为了解决这么大的开销,vue在这里讨个巧,将对应的节点用JS封装成了vnode对象,通过对vnode对象的操作,进而一次性完成对dom的更改,减少了页面性能的消耗。

这样,便是所谓虚拟DOM了,如果看不懂,我们直接放个简单的例子,例子一看就能明白。

这是真实dom

1
2
3
4
5
<ul class="list">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>

这是虚拟dom

1
2
3
4
5
let vnode = h('ul.list', [
h('li','a'),
h('li','b'),
h('li','c'),
])

这里放个补充说明,平时我们开发中一定见过的。

上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆; 开发中常见的现实场景,render函数渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 案例1 vue项目中的main.js的创建vue实例
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");

//案例2 列表中使用render渲染
columns: [
{
title: "操作",
key: "action",
width: 150,
render: (h, params) => {
return h('div', [
h('Button', {
props: {
size: 'small'
},
style: {
marginRight: '5px',
marginBottom: '5px',
},
on: {
click: () => {
this.toEdit(params.row.uuid);
}
}
}, '编辑')
]);
}
}
]

这里的render函数,实际上就是将虚拟的节点转换为真实dom加入到了dom树中,这样,我们就不需要更新整个dom数,而是只更新对应的节点即可。

这里关于虚拟dom的实现,这里就不贴源码了,感兴趣的可以自己去翻snabbdom,vue2内部使用的虚拟DOM就是改造的Snabbdom。

哦,可能有的面试官会有个灵魂发问:虚拟dom更新一定就比真实dom更新来的快吗?

这个要看情况,如果dom数足够的复杂,用虚拟dom会自然会更快,但是简单操作一个dom节点的话,比如换换文字什么,这种肯定是真实dom操作更快一点。

不过,这个问题也就图一乐,为了这点性能开销单独写个dom操作一个不重要的节点,多少有点大可不必了。

diff算法

重头戏了来了,虚拟dom只是个简单的开胃菜,重头戏还是得是diff算法。

如果你vue的开发者,现在你拿到了虚拟dom,你要如何知道前后两者发生了什么地方的改变呢?

有些朋友可能会说,这有什么难的,直接拿着每个节点挨个遍历比对就可以了,最暴力最直接的手法即可。

这种做法其实就是传统的diff算法,简单粗暴,直接有效。

不过,我们这里先看看vue是怎么实现虚拟dom的比对的。

本图来自于B站思学堂UP的整理,他的这张图是我目前看到整理的最清晰的图片,我大致回想了一下,和我以前看到的理解差不多,只要看懂了这张图,大致就明白vue的diff算法是这块是怎么一回事了。

00325814-9fc9-490b-8b4d-34d765016118 (1)

vue的diff算法优化

snabbdom做的的优化,相对于传统简单粗暴的遍历法,根据dom的特性做了一个优化,那就是同级比对。

如果我对应的节点已经发生了改变,那这个节点之下的子节点我还有什么必要进行比对呢?这样的话,就省下了了很多性能,所以,vue2出道的时候,vue2这套diff算法也一度是热门面试题,只不过现在是2024年了,大家很少提及相关的事情了。

关于vue这套算法的特点,这里不细讲,这里稍微总结了一下,便于大家面试时候回答。

  • Snbbdom根据DOM的特点对传统的diff算法做了优化
  • DOM操作时候很少会跨级别操作节点
  • 只比较同级别的节点

updateChildren

在上边的图中,我们注意到了一件事,那就是新旧节点比对,都有子节点,于是转入了updateChildren()的方法。

这里便是重点,前边的三种情况都好说,换文本,删除或新增节点,很干脆快速的就能完成。

但是如果之下还有子节点,要怎么继续判断才能最省性能的方式完成对dom的操作呢?

就是首尾指针法,这种方法个人认为非常切中我们平日操作数组的习惯,算是大幅提高效率的一种方法。

这里稍微浅谈一下所谓的首尾指针法,就是将数组分为首尾两个指针。

vue的在updateChildren的时候,会将新旧两个虚拟dom树做比对,这时候,取新旧两个dom树的数组首尾,于是得到了四个节点:新前,新后,旧前,旧后。

然后按照如下顺序执行:新前与旧前,新后与旧后,新后与旧前,新前与旧后。

c134f0af-408f-4310-93bc-a83c89cc1979

尚硅谷的【尚硅谷】Vue源码解析之虚拟DOM和diff算法,我个人比较推荐他的视频,讲的很清楚。

我这里大致通过这张图,我们就很能明白比对的方式了,这里暂时就不按照其他讲解放箭头图了,总之,通过前后对比的方法,首尾指针接近,直至循环完成之后,将旧树中未命中的旧节点移除,将新树中的新增节点加入。

在比对完成之后,只需要操作一次真实dom,而且只操作没有key标记的DOM,这样可以最小限度的减少对dom树的破坏,减少页面开销,这也是我们在之前遇到的vue2如果不绑定Key的时候,为什么一部分dom更新会出问题的原因。

key的作用

这里,又联系到之前提到v-for相关的说法了,因为你给节点加了key,所以,虚拟dom在遍历的时候,就很容易能找到对应的节点不会找错,因此,更快更准。

而如果你用索引作为key,自然会出现一个问题,那就是索引不具有唯一性,因此索引作为key会导致渲染出BUG的原因便在于此。

  • Diff操作可以更加快速
  • Diff操作可以更加准确(避免渲染错误)
  • 不推荐使用索引作为key,因为索引被修改后不具有唯一性

结语

虚拟dom和diff算法,说破天,都是为了节省对dom操作的性能浪费而应对的办法,大多业务层面的前端开发者可以图个乐子了解一下。

面试中问到,其实也是可以理解的,毕竟这也是vue的看家本事之一,所以对自己的工具原理不稍微了解下,确实也说不过去。

不过,我们实际上其实用不到太多,做个大概得了解就可以,如果需要对源码进行深入拓荒的,可以尝试对源码层面进行了解。

参考

DIff算法看不懂就一起来砍我(带图)

6分钟彻底掌握vue的diff算法,前端面试不再怕!

【尚硅谷】Vue源码解析之虚拟DOM和diff算法