这个也是较为常考的面试八股题,很早之前面试准备时候了解过,如今也算是捡起来重新看看了。
正文
本篇算是Vue响应式原理浅析那篇的补充,在响应式原理中,着重讲了vue实现响应式原理的核心思路,但是在针对页面渲染这块没怎么细说。
虚拟dom渲染这块,面试核心便是算法的思路,真让你手写实现,大概率也最多就是个伪代码的思路图,不可能真让大家去实现的。
所以,在本篇,本文并不深入探讨算法的源码实现,更多的是讲明白这个渲染的思路。
虚拟DOM
虚拟dom在响应式原理那边解释过,如果直接操作dom进行响应式的修改的话,对页面的性能开销会很大。
作为前端开发,我们都知道的,重排是比重绘性能开销大的多的操作,如果每个变动都要直接操作整个DOM树进行替换,那么毫无疑问,vue绝对会被扫入历史垃圾堆。
为了解决这么大的开销,vue在这里讨个巧,将对应的节点用JS封装成了vnode
对象,通过对vnode
对象的操作,进而一次性完成对dom的更改,减少了页面性能的消耗。
这样,便是所谓虚拟DOM了,如果看不懂,我们直接放个简单的例子,例子一看就能明白。
这是真实dom
1 | <ul class="list"> |
这是虚拟dom
1 | let vnode = h('ul.list', [ |
这里放个补充说明,平时我们开发中一定见过的。
上面的h函数大家可能有点熟悉的感觉但是一时间也没想起来,没关系我来帮大伙回忆; 开发中常见的现实场景,render函数渲染
1 | // 案例1 vue项目中的main.js的创建vue实例 |
这里的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算法是这块是怎么一回事了。
vue的diff算法优化
snabbdom做的的优化,相对于传统简单粗暴的遍历法,根据dom的特性做了一个优化,那就是同级比对。
如果我对应的节点已经发生了改变,那这个节点之下的子节点我还有什么必要进行比对呢?这样的话,就省下了了很多性能,所以,vue2出道的时候,vue2这套diff算法也一度是热门面试题,只不过现在是2024年了,大家很少提及相关的事情了。
关于vue这套算法的特点,这里不细讲,这里稍微总结了一下,便于大家面试时候回答。
- Snbbdom根据DOM的特点对传统的diff算法做了
优化
- DOM操作时候很少会跨级别操作节点
- 只比较
同级别
的节点
updateChildren
在上边的图中,我们注意到了一件事,那就是新旧节点比对,都有子节点,于是转入了updateChildren()
的方法。
这里便是重点,前边的三种情况都好说,换文本,删除或新增节点,很干脆快速的就能完成。
但是如果之下还有子节点,要怎么继续判断才能最省性能的方式完成对dom的操作呢?
就是首尾指针法,这种方法个人认为非常切中我们平日操作数组的习惯,算是大幅提高效率的一种方法。
这里稍微浅谈一下所谓的首尾指针法,就是将数组分为首尾两个指针。
vue的在updateChildren的时候,会将新旧两个虚拟dom树做比对,这时候,取新旧两个dom树的数组首尾,于是得到了四个节点:新前,新后,旧前,旧后。
然后按照如下顺序执行:新前与旧前,新后与旧后,新后与旧前,新前与旧后。
尚硅谷的【尚硅谷】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的看家本事之一,所以对自己的工具原理不稍微了解下,确实也说不过去。
不过,我们实际上其实用不到太多,做个大概得了解就可以,如果需要对源码进行深入拓荒的,可以尝试对源码层面进行了解。