作为前端开发者,无论是否使用过Vue
框架作为技术栈,都应该听过说Vue
的虚拟Dom
。
如果在面试的时候被面试官问:Vue
为什么要使用虚拟DOM
?那么在回答这个问题之前我们应该先了解js操作dom
的方式和性能差异;
操作DOM的方法和性能
JavaScript
操作DOM
一般采用一下两种方式:
- 通过原生的
JavaScript
代码直接操作DOM
; - 通过html字符串拼接+innerHTML直接操作
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
使用innerHTML方式
const html = `<div>使用innerHTML</div>`
div.innerHtml = html;
虚拟DOM
使用虚拟DOM操作html
const virtualDOM = {
tag: 'div',
children: [{ children: '使用虚拟DOM操作html' }]
}
// render 函数将虚拟 DOM 创建为真实 DOM ,并将其插入到文档中
render(virtualDOM)
以上便是三种操作DOM的方式,在写法上有些不同,下面重点分析一下性能问题;
性能差异对比
首先有两个要点需要声明:
- js的操作性能要远远大于dom的操作,他们两个不是一个量级;
- js原生操作dom的方法应该除去innerHTML方法,因为innerHTML较为复杂;
最快的性能
修改dom最直接的方式就是用原生的方法,也是最优的性能选择;
div.textContent = "使用js-textContent修改dom文本"
因为我们知道dom结构中哪个地方需要修改,要修改成什么内容,直接使用js操作dom就是最优的
虚拟DOM的性能
使用虚拟DOM的方式去操作DOM,其实就是找出来虚拟DOM
前后的区别,然后更新一下,请看下面示例代码:
<!-- 原始的dom代码如下 -->
<div>虚拟DOM修改前</div>
<!-- 修改后的dom代码 -->
<div>虚拟DOM修改后</div>
这里需要说一下,虚拟DOM
的底层还是使用的原生的js去操作的DOM
,只不过我们对修改做了一层封装。所以虚拟DOM
的性能要低于原生js操作DOM
的方式;
这里我们可以得出的结论是:
- 原生js操作
DOM
的性能 = js操作DOM
的性能; - 虚拟
DOM
的性能 = js找出DOM
前后差异的性能 + js操作DOM
的性能;
根据上面的公式可知,只有当js找出DOM前后差异的性能
消耗为0时,两者的性能才会相等,但是永远不会超过原生js操作DOM的性能
,所以我们在框架中使用算法去对比虚拟DOM
的差异时,就是无限优化并使其的性能消耗降到最低(这也是后面为什么使用diff
算法的起因)
至于为什么还要使用虚拟DOM
,这个问题在文章的最后再说。
innerHTML的性能
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直接去开发,之前是使用JQ
,现在是使用Angular
、Vue
、React
等等。这主要是因为减少开发者的心智负担,方便快速开发与代码维护。
命令式
原生的JS代码一般都是命令式的,就是我想做什么操作,我就发出什么指令,关注的是过程。一个典型的命令式框架(库)是JQ,例如我想修改一个标签中的文本:
$('#divTag').text('修改后的文案')
命令式代码基本上就是一条条指令,在告诉程序我要干什么吗。如果我又想修改一下className呢?
$('#divTag').className('add-class')
声明式
声明式更关注的是结果,我们不在乎实现的过程是什么,我们只需要告诉框架,我们想要的结果,然后帮我来实现就行。同样的代码我们使用声明式来实现一下。
<div id="divTag" class="add-class">修改后的文案</div>
好了,这就实现了我们想要的结果,是不是可读性很好。至于他是如何实现的这个结果,我们一般并不需要关心,不过这里还是需要说明一下,Vue
底层还是使用的命令式的代码帮我们做的封装,毕竟命令式的性能是最优的。
总结
Vue为什么使用虚拟DOM
作为前端开发者,无论是否使用过Vue
框架作为技术栈,都应该听过说虚拟DOM
这个概念。
如果在面试的时候被面试官问:Vue
为什么要使用虚拟DOM
?那么在回答这个问题之前我们应该先了解js操作DOM
的方式和性能差异;
操作DOM的方法和性能
JavaScript
操作DOM
一般采用一下两种方式:
- 通过原生的
JavaScript
代码直接操作DOM
; - 通过html字符串拼接+innerHTML直接操作
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
使用innerHTML方式
const html = `<div>使用innerHTML</div>`
div.innerHtml = html;
虚拟DOM
这里先解释一下什么是虚拟DOM,其实虚拟DOM就是一段用来描述真实的DOM的JavaScript代码(对象)并能根据一定的规则转换成真实的DOM节点
。
使用虚拟DOM操作html
const virtualDOM = {
tag: 'div',
children: [{ children: '使用虚拟DOM操作html' }]
}
// render 函数将虚拟 DOM 创建为真实 DOM ,并将其插入到文档中
render(virtualDOM)
以上便是三种操作DOM的方式,在写法上有些不同,下面重点分析一下性能问题;
性能差异对比
首先有两个要点需要声明:
- js的操作性能要远远大于dom的操作,他们两个不是一个量级;
- js原生操作dom的方法应该除去innerHTML方法,因为innerHTML较为复杂;
最快的性能
修改dom最直接的方式就是用原生的方法,也是最优的性能选择;
div.textContent = "使用js-textContent修改dom文本"
因为我们知道dom结构中哪个地方需要修改,要修改成什么内容,直接使用js操作dom就是最优的
虚拟DOM的性能
使用虚拟DOM的方式去操作DOM,其实就是找出来虚拟DOM
前后的区别,然后更新一下,请看下面示例代码:
<!-- 原始的dom代码如下 -->
<div>虚拟DOM修改前</div>
<!-- 修改后的dom代码 -->
<div>虚拟DOM修改后</div>
这里需要说一下,虚拟DOM
的底层还是使用的原生的js去操作的DOM
,只不过我们对修改做了一层封装。所以虚拟DOM
的性能要低于原生js操作DOM
的方式;
这里我们可以得出的结论是:
- 原生js操作
DOM
的性能 = js操作DOM
的性能; - 虚拟
DOM
的性能 = js找出DOM
前后差异的性能 + js操作DOM
的性能;
根据上面的公式可知,只有当js找出DOM前后差异的性能
消耗为0时,两者的性能才会相等,但是永远不会超过原生js操作DOM的性能
,所以我们在框架中使用算法去对比虚拟DOM
的差异时,就是无限优化并使其的性能消耗降到最低(这也是后面为什么使用diff
算法的起因)
至于为什么还要使用虚拟DOM
,这个问题在文章的最后再说。
innerHTML的性能
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直接去开发,之前是使用JQ
,现在是使用Angular
、Vue
、React
等等。这主要是因为减少开发者的心智负担,方便快速开发与代码维护。
命令式
原生的JS代码一般都是命令式的,就是我想做什么操作,我就发出什么指令,关注的是过程。一个典型的命令式框架(库)是JQ,例如我想修改一个标签中的文本:
$('#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
这个概念。当然这也仅仅是其中的一部分使用的理由,因为还有响应式等等。。。