作为前端开发者,无论是否使用过Vue
框架作为技术栈,都应该听过说虚拟DOM
这个概念,也称VDom。
虚拟DOM最先是由facebook(现在叫Meta)技术团队提出的,最先运用在React框架中,之后Vue2.0中引入了虚拟DOM的概念。
虚拟 DOM 加持下的MVVM框架页面呈现:
进入VDom之前,我们先来了解下JavaScript操作DOM
的方式和性能差异;
JavaScript
操作DOM
一般采用以下两种方式:
JavaScript
代码直接操作DOM
;DOM
;在我们前端开发者最初接触JavaScript
时,都将学习如何使用原生js对DOM
进行操作(增、删、改、查、事件绑定等等)。
例如有如下代码:
<div id="testEle">这是div的文本内容div>
// 修改div的内容
const ele = documont.querySelector('#testEle');
div.innerText = "使用js-innerText修改dom文本"
// or
div.textContent = "使用js-textContent修改dom文本"
使用innerHTML方式
const html = `使用innerHTML`
div.innerHtml = html;
这里先解释一下什么是虚拟DOM,其实虚拟DOM就是一段用来描述真实的DOM的JavaScript代码(对象)并能根据一定的规则转换成真实的DOM节点
。
使用虚拟DOM操作html
const virtualDOM = {
tag: 'div',
children: [{ children: '使用虚拟DOM操作html' }]
}
// render 函数将虚拟 DOM 创建为真实 DOM ,并将其插入到文档中
render(virtualDOM)
以上便是三种操作DOM的方式,在写法上有些不同,下面重点分析一下性能问题;
首先有两个要点需要声明:
修改dom最直接的方式就是用原生的方法,也是最优的性能选择;
div.textContent = "使用js-textContent修改dom文本"
因为我们知道dom结构中哪个地方需要修改,要修改成什么内容,直接使用js操作dom就是最优的
使用虚拟DOM的方式去操作DOM,其实就是找出来虚拟DOM
前后的区别,然后更新一下,请看下面示例代码:
<div>虚拟DOM修改前div>
<div>虚拟DOM修改后div>
这里需要说一下,虚拟DOM
的底层还是使用的原生的js去操作的DOM
,只不过我们对修改做了一层封装。所以虚拟DOM
的性能要低于原生js操作DOM
的方式;
那为什么在这里需要额外加一层封装呢?
首先,我们要知道在前端性能中最大的就是频繁操作DOM,频繁变动DOM会造成浏览器的回流与重回,因此这一层抽象,在“频繁操作”过程中尽可能地将DOM差异计算得到最终结果,以确保DOM不会出过度操作导致性能消耗,比如DOM变更中出现A-B-C-A-D情况,在VDom中直接操作A-D情况,极大减少了中间过程的浪费。从框架角度看,中间层对Dom操作的抽象,可以解放开发者针对DOM的手动操作,同时在适配更多“中间层”方面可以得到更多平台支持,实现框架的跨平台底层支撑。
通过以上案例我们可以得出的结论是:
DOM
的性能 = js操作DOM
的性能;DOM
的性能 = js找出DOM
前后差异的性能 + js操作DOM
的性能;根据上面的公式可知,只有当js找出DOM前后差异的性能
消耗为0时,两者的性能才会相等,但是永远不会超过原生js操作DOM的性能
,所以我们在框架中使用算法去对比虚拟DOM
的差异时,就是无限优化并使其的性能消耗降到最低(这也是后面为什么使用diff
算法的起因)
至于为什么还要使用虚拟DOM
,这个问题在文章的最后再说。
innerHTML
不是简单的赋值,页面要想渲染出来html文档的内容,就先要将innerHTML
的内容解析成DOM
结构树,然后再插入到DOM
文档结构中去,当然插入前需要先删除旧的文档结构。这里面有几个关键问题:
DOM
结构树DOM
结构DOM
结构以上三个关键点其中解析DOM结构树
是属于js级别的计算,另外后面两个则属于DOM
级别的操作计算。
现在将三者的性能消耗进行一个对比分析:
操作方式 | JS级别 | DOM 级别 |
---|---|---|
原生JS | 纯JS运算 | DOM 运算 |
innerHTML |
纯JS运算html字符串DOM 解析 |
DOM 创建 |
虚拟DOM |
创建虚拟DOM |
DOM 创建 |
上面表格对比了创建一个新的页面的性能消耗对比,先抛开原生JS的方式(因为该方式上文已经说了,是最优的)仅对比虚拟DOM
和innerHTML
的性能损耗差异;
innerHTML
的性能损耗 = js运算html字符串拼接性能损耗 + DOM
创建的性能损耗;DOM
的性能损耗 = js创建虚拟DOM
对象的性能损耗 + DOM
创建的性能损耗;单从创建页面这个维度对比,两者貌似性能差异没有太大的区别;
下面我们从另外一个维度来进行对一下:更新维度
;
使用innerHTML
更新页面的过程是重新构建html字符串,再重新设置DOM
元素的innerHTM
属性,这其实是在说,哪怕我们只更改了一个文字,也要重新设置innerHTML
属性。而重新设置innerHTML
属性就等价于销毁所有旧的DOM
元素,再全量创建新的DOM
元素。
再来看虚拟DOM
是如何更新页面的。它需要重新创建JavaScript对象(虚拟DOM
),然后比较新旧虚拟DOM
,找到变化的元素并更新它。
另外一个因素是和页面的代码体量有关系。对于innerHTML
来说,页面大小越大,更新的时候性能消耗也就越大。而虚拟DOM
紧紧需要更新变动的部分,这就与需要更新的数据量有关,而与页面大小没有关系。所以这个结论就是页面越大,innerHTML
的性能消耗就越大,远远超过虚拟DOM
的性能消耗。
操作方式 | JS级别 | DOM 级别 |
---|---|---|
原生JS | 纯JS运算 | DOM 运算 |
innerHTML |
纯JS运算html字符串DOM 解析 |
销毁DOM +DOM 创建,与页面大小有关 |
虚拟DOM |
创建虚拟DOM +Diff算法 |
变化部分的DOM 更新,与更新的数据量有关 |
根据上面的性能消耗分析对比可以知道:原生JS < 虚拟DOM
< innerHTML
。那么既然如此为什么我们不使用原生JS呢,这就牵出另外一个重要的问题可维护性
。
原生的JS操作是性能最优的,但是在实践的业务项目开发中,我们很少使用原生的JS直接去开发,之前是使用JQuery
,现在是使用Angular
、Vue
、React
等等。这主要是因为减少开发者的心智负担,方便快速开发与代码维护。
原生的JS代码一般都是命令式的,就是我想做什么操作,我就发出什么指令,关注的是过程。一个典型的命令式框架(库)是JQuery,例如我想修改一个标签中的文本:
$('#divTag').text('修改后的文案')
命令式代码基本上就是一条条指令,在告诉程序我要干什么吗。如果我又想修改一下className呢?
$('#divTag').className('add-class')
声明式更关注的是结果,我们不在乎实现的过程是什么,我们只需要告诉框架,我们想要的结果,然后帮我来实现就行。同样的代码我们使用声明式来实现一下。
<div id="divTag" class="add-class">修改后的文案div>
好了,这就实现了我们想要的结果,是不是可读性很好。至于他是如何实现的这个结果,我们一般并不需要关心,不过这里还是需要说明一下,Vue
底层还是使用的命令式的代码帮我们做的封装,毕竟命令式的性能是最优的。
在Vue项目开发时,推荐使用模版语法,因为模版语法比直接使用虚拟DOM更加直观(虚拟DOM比模版语法更加灵活)。模版语法需要通过编译器转换成虚拟DOM,再使用渲染器渲染成真实的DOM节点,而虚拟DOM较传统的innerHTML最大的优势是数据更新节段,它紧紧更新变化的部分,性能消耗更小。在权衡了性能消耗、代码的可维护性,Vue
(包括React
等)主流框架,使用了虚拟DOM
这个概念。当然这也仅仅是其中的一部分使用的理由,因为还有响应式等等。