虚拟DOM和DOM diff

257 阅读6分钟

虚拟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的优点

  1. 减少DOM操作的次数: 假如要添加1000个节点,原生DOM需要一个一个操作,即操作1000次,但是虚拟DOM可以优化成1次就搞定添加1000个节点,所以使用虚拟DOM可以减少DOM操作的次数。
  2. 减少DOM操作的范围: 假如页面上已有990个节点,我们要新增10个节点,如果用原生DOM来操作,无法区分哪些节点是旧的,哪些节点是新增的。而Vue或React可以通过对比区分开来,如果是原来就有并且没有改变的节点,就不用重复更新了,这个样就减少了DOM操作的范围。
  3. 跨平台:因为虚拟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的原因。