虚拟DOM是什么
虚拟DOM是对应原生DOM的一个概念。
回忆一下原生DOM是啥:
DOM是Document Object Model,也就是文档对象模型,是一个结构化文本的抽象。我们在动态修改网页内容的时候,其实就是我们在操作DOM。
而虚拟DOM(Virtual DOM)是一层对真实DOM的抽象,是一个JS对象,包含了tag、props、children等属性。
总结:虚拟DOM就是一个普普通通的JS对象,这个对象可以代表DOM树,对象里含有标签名、标签上的属性、事件监听、子元素和其他属性。
不直接用DOM,而是通过js的对象模拟DOM中的节点,在数据更新的时候,渲染得到新的Virtual DOM,与上一次得到的Virtual DOM进行diff比较,把所有在DOM上进行变更的地方找出来,然后有选择性的进行渲染。
关于DOM的争议
很多人认为DOM操作速度慢,而虚拟DOM操作速度比较快。
其实我们要看具体的过程来判断DOM操作是不是真的慢:
我们进行DOM操作时,首先在JS引擎里进行DOM操作,然后把结果传给渲染引擎,然后渲染引擎把结果渲染到浏览器页面上。
在js引擎中,DOM的操作并不慢,很快就能得到DOM操作的结果。DOM操作慢就慢在渲染的过程,因为只要改动一行数据,渲染引擎就要全部重新渲染。
(浏览器在渲染的过程中,页面是无法交互的,必须要渲染完成才能进行交互,这样体验就比较差。)
大多数情况下,虚拟DOM比原生DOM快的原因在于:虚拟DOM中需要更新的节点要比原生的DOM中需要更新的节点要少,所以浏览器渲染的时间更短。所以说虚拟DOM的优点并不在于单次的操作,而是在大量、频繁的数据更新的情况下,能够通过对比的算法,把多次操作合并成一次操作,从而能够对视图进行合理、高效的更新。
虚拟DOM长啥样
//React中的虚拟DOM:
const vNode = {
key:null,
props:{
children:[ //子元素们
{type:"span",...},
{type:"span",...}
],
className:"red",//标签上的属性
onClick:()=>{}//事件
},
ref:null,
type:"div",//标签名或组件名
...
}
//Vue中的虚拟DOM
const vNode = {
tag:"div",//标签名或组件名
data:{
class:"red",//标签上的属性
on:{
click:()=>{}//事件
}
},
children:[//子元素
{tag:"span",...},
{tag:"span",...}
],
...
}
如何创建虚拟DOM
//React.createElement
createElement("div",{className"red",onClick:()=>{}},[
createElement("span",{},"span1"),
createElement("span",{},"span2"),
])
//Vue
h("div",{
class:"red",
on:{
click:()=>{}
},
},[h("span",{},"span1"),h("span",{},"span2")],)
让创建虚拟DOM的方式简单一些
//React
<div className="red" onClick={fn}>
<span>span1</span>
<span>span2</span>
</div>
//Vue Template
<div class="red" @click="fn">
<span>span1</span>
<span>span2</span>
</div>
这样写以后,React可以通过babel来把代码转为createElement形式,vue可以通过vue-loader转为h形式。
虚拟DOM的优点
- 减少DOM操作的次数: 假如要添加1000个节点,原生DOM需要一个一个操作,即操作1000次,但是虚拟DOM可以优化成1次就搞定添加1000个节点,所以使用虚拟DOM可以减少DOM操作的次数。
- 减少DOM操作的范围: 假如页面上已有990个节点,我们要新增10个节点,如果用原生DOM来操作,无法区分哪些节点是旧的,哪些节点是新增的。而Vue或React可以通过对比区分开来,如果是原来就有并且没有改变的节点,就不用重复更新了,这个样就减少了DOM操作的范围。
- 跨平台:因为虚拟DOM本质上是一个JS对象,所以不仅仅可以用于变成DOM,还可以应用于小程序、IOS应用和安卓应用等其他的应用中。
虚拟DOM的缺点
需要额外的创建函数来创建,比如createElement和h,写起来比较麻烦,但是可以通过打包工具在简化。但是简化方法的缺点就是过于依赖打包工具,需要额外的构建过程,不然简化的语法JS是不认识的。
DOM diff是什么
DOM diff是一种虚拟DOM的对比算法。
如果把虚拟DOM想象成树形的话:
<div :class="red">
<span v-if="y">Hello</span>
<span>World</span>
</div>
当div的class属性从red变成green时:
DOM diff发现:div标签的类型没变,还是div,但是对应的DOM属性变了,所以更新一下属性。而子元素没任何改变,所以压根不用更新,即总过程只需要改变一个div的属性,其他的不动。
当第一个span里的v-if="y"变成了false后:
正常人的想法是:删除了第一个span,就直接把第一个span删除了不就完事了吗,第二个span还在原来的位置。
但是计算机却是这样认为的:你并不是删除第一个span,而是第一个span的内容从Hello改成了World,所以计算机先修改掉第一个span的内容,然后再把第二个span删除。
所以DOM diff也可以看成一个函数,我们称这个函数为pacth:
pacth(oldVNode,newVNode)
让pacth()的结果为一个pacthes,pacthes就是要运行的DOM操作: pacthes = pacth(oldVNode,newVNode)
pacthes可能长这样:
[
{type:"INSERT",vNode:...},
{type:"TEXT",vNode:...},
{type:"PRORS",propsPacth:...},
]
DOM diff的简易逻辑
DOM diff的可能的大概逻辑是这样的:
Tree diff: 将新旧两颗DOM树逐层的对比,找到哪些节点需要更新,如果需要更新的节点是组件,就看Component diff。如果需要更新的节点是标签,就看Element diff。
Component diff:
如果需要更新的节点是组件,就先看组件类型,类型不同就直接替换,把旧的删除,把新的给整出来。如果类型相同,就只更新里面的属性,然后继续深入,使用Tree diff(递归)。
Element diff:
如果需要更新的节点是标签,就看标签名,如果标签名不同,就直接替换,相同则只更新属性,然后继续深入,进入标签后代,继续做Tree diff(递归)。
DOM diff的优点
通过使用DOM diff,我们可以只更新发生改变的节点,节省多余的DOM操作,提高效率。
DOM diff的缺点
DOM diff在同级节点中对比会有一些bug:
假如有三个同级节点,当我们删除第二个节点后,在DOM diff算法中,并不认为要删除第二个节点,而只是把第二个节点的内容变成了第三个节点的内容,被删除的却是第三个节点。
如何避免这个问题?我们可以给每一个节点上加一个唯一的属性key,来标识每个节点。这也就是为什么vue中的v-for语句后必须声明key的原因。