现在我们的项目中大多数都是用的react和vue等框架,我们都知道这些框架的主要核心之一是实现了虚拟dom。今天就让我们一起研究一下这个虚拟dom具体是如何实现的吧
什么是虚拟dom
虚拟dom(virtual-dom简称vdom)就是通过使用js来模拟真实的dom,通过这种技术,我们可以精准的改变我们需要改变的dom结构,而不是粗暴的将整个dom结构改变,从而减少dom开销。另一方面,dom结构用js来控制的话,我们就可以实现数据来控制结构的改变。比如我们的react。
虚拟DOM是react框架的非常重要的一个特性之一,vdom的推广也是得益于react的出现。主要是为了解决手动操作dom而带来的性能问题
为什么要用虚拟dom
首先,我们看一下浏览器在渲染一个HTML文件需要哪些步骤。
所有浏览器的工作流程都差不多,大致分为这5个部分。创建DOM树 -> 创建CSSOM -> 运行JS -> 渲染树 -> 实现布局 -> 绘制页面
在render树和渲染dom树和css树的过程中,它们不是完全独立的,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。
在使用原生api或者用JQuery操作dom树的时候,浏览器会从构建DOM树开始从头到尾执行一遍流程。DOM操作非常昂贵,而js的运行效率非常高,可以用js来模拟DOM结构,从而减少dom结构的操作。
来看看为什么操作真实dom是一件非常耗费性能的事情
OMG,我们看到一个div竟然有3000多个属性,还要不要活了。果然还是老话说的对,“宁可js操作一万遍,不能操作dom一遍”。所以虚拟dom就是为了解决dom结构操作消耗太大,影响浏览器性能的问题
即使计算机硬件一直在更新迭代,但也扛不住dom结构这么任性的造呀,频繁操作还是会出现页面卡顿,影响用户的体验。
虚拟dom如何实现
上面说的问题这么严重,要怎么解决才好呢?正所谓“卤水点豆腐,一物降一物”说的就是这个道理,如此邪恶的耗费性能,不能放任不管,虚拟dom就是为了降服这个问题而诞生的。接下来,我们介绍一下虚拟dom是如何实现的吧
此篇文章以snabbdom为例子来说一下虚拟dom是如何实现的。
为啥选择用snabbdom.js库来介绍虚拟dom呢?
- snabbdom.js库是专门实现虚拟dom,源码非常简短
- 非常火爆的vue实现虚拟dom就是借鉴的snabbdom.js库实现的
首先,我们看一下snabbdom官方给的例子
这里可以列出来两个核心函数,即h()函数和patch()函数。首先我们看一下h()函数
h()函数其实主要是提供了一个工具函数,让我们方便的创建一个vnode对象,实现虚拟dom结构。
来个栗子: 一般我们在html下的DOM结构是这样的
h()函数的实现方式是这样的:
经过解析之后的结果:
值得注意的一点是: JS模拟的DOM节点并没有模拟实际DOM节点上的属性、方法,而只是模拟了其中一部分和数据相关的属性和方法;从而达到减小耗费性能的目的
现在有这样一个场景:有个列表的信息是序号、姓名、地址,如下样子
现在我们想把第二个数据的序号从222改成555,将第三条数据的广东改成米国,我们想的就是,只要将第二条的222修改成555,第三条的广东修改成米国,别的保持不变就可以了,对不对!!!但是,实际上是这样的吗?
可以看到,我们需要的数据已经发生变化,但是,我们同时观察dom结构的变化发现,ul的结构闪烁了一下,说明dom结构是被重新删除又重新创建的,而不是只修改我们想要修改的内容,这样的操作无遗就是操作了我们昂贵的dom结构,这样的操作万万使不得呀
现在我们通过一个实例看看虚拟dom是否实现了我们想要的结果?
我们现在的是这个样子的,如下图
我们希望点击button的时候,可以将item 2 修改成item 哈哈,然后在添加一个item3,如下图
通过操作虚拟dom,还是不是以前那种简单粗暴的方法,直接将整个item系列删除,然后在创建呢?让我们看一下实际的渲染过程是啥样的吧
哎呦,虚拟dom果然是优秀。通过点击button,我们可以看到,这次的操作只是修改了我们想让它改变的部分,我们不想改变的结构,根本就没有发生任何变化。显然,已经达到了我们预期的结果,让我们看一下是如何实现的吧
通过上面的对比实例我们可以看出,由于实际dom的渲染过程是先删除原来的结构在创建新的dom结构,而且每一个节点上的属性又特别多,如果我们大面积操作dom结构的话,就会出现多余加载dom结构的现象,而且也会引发性能问题,而用js实现虚拟dom,则会减少dom操作。
接下来,我们看一下snabbdom实现虚拟dom的核心api是什么呢
虚拟dom的核心api
snabbdom实现虚拟dom有三个比较核心的部分分别为:h()函数,patch()函数和diff算法
h()函数的作用是将js模拟的DOM结构,转换成虚拟DOM结构,再通过patch()函数将虚拟DOM转换成真实的DOM结构渲染到页面中,接下来,我们看看patch函数是做什么的呢?
path()函数的执行分为两个阶段。两个阶段都传递两个参数
第一个阶段是虚拟DOM首次渲染。patch(container,vnode),放在真是DOM的container和生成的vnode,此时此刻patch()函数的主要作用是将初次生成的真实的DOM结构挂载到指定的container上。
第二个阶段是更新DOM节点。patch(vnode,newVnode),此阶段的patch()函数,根据vnode和newVnode传进来的参数判断DOM节点是否发生变化,来改变DOM结构,那么patch函数是如何实现对比两个vnode结构是否发生变化,从而对真实的DOM节点进行更新呢?这里就涉及到diff算法了
diff算法也是虚拟dom实现过程中一个非常核心的部分,通过对比新旧dom,从而判断是否要改版dom结构。后续会有diff算法详细介绍的文章,这里就不多赘述了
为何要减少dom开销
我们为什么要用虚拟dom来减少dom的开销呢?
首先,DOM的渲染和浏览器的架构是有关系的,在webkit的浏览器架构中DOM模块和js的模块是互相独立且分割的,因此每次操作DOM的开销要比单纯的操作一次js开销要大。
第二: 在整个前端项目中,浏览器的重绘和重排非常耗费性能,减少重绘和重排也是我们前端需要做的优化。
最后:像一些大型的项目,销毁dom结构是非常常见的事情,在之前的jQuery时代,总有一些渲染列表的行为存在,可想而知,减少dom操作在我们前端世界有多么重要。
现在我们直观的看一下,用实际dom操作和虚拟dom操作,在dom渲染的过程中,耗费时间的大小
题目: 页面上有1000个dom结构,现在想要改一下前10个dom,如图所示:
点击修改后,前10个发生变化,看一下真实dom和虚拟dom在渲染过程中,需要的时间:
通过实例我们可以看到,操作真实dom是虚拟dom操作时间的10倍多。而且这才是修改10个dom,平时我们做的项目,不止是修改10个dom这么简单,可以看出,减少dom开销,我们势在必行
虚拟dom减少dom的开销有什么好处呢?
虚拟dom是为了解决浏览器性能而设计出来的,就像之前的更改dom结构的例子,虚拟dom不会立即更改dom结构,而是将更新的diff内容保存到一个js对象中,最终将这个js对象一次性渲染到dom树上,从而减少dom结构的运算量。所以,用JS对象模拟DOM节点的好处是,操作js中的更新对象,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
文章写到这里就已经接近尾声了,希望这篇文章能帮助小伙伴们进一步的了解虚拟DOM。既然大家都看到这里了,不妨在动动手给点个赞鸭!同时,再安利大家一句话
真正最重要的任务永远只有一个——那个真正对你目标实现有帮助的任务