由浅及深实现虚拟DOM和DOM-Diff
一、前言
随着前端框架比如Vue和React的不断发展,虚拟DOM和DOM-Diff也随着这些框架被越来越多的人重视。在学习和面试的过程中,越来越成为我们无法回避的知识点。面试时经常会被问到:了解虚拟DOM吗?知道Vue和React的虚拟DOM是什么样的吗?知道他们的DOM-Diff是如何实现的吗?如果你没有认真去看过他们的源码,可能会一问三不知。但是去看源码对一些新手又会觉得有点困难,而且难以理解。因此,我的一贯的思想是,如果你想要熟练掌握一个东西,最好的方式就是去实现一个简单的这种东西。
在我之前的文章中,我想要学习webpack,那么我就手动实现一个简易的模块打包器。我想了解loader
,那么我就手动去制作了一个loader由浅及深实现一个loader。通过这些简单地实现,可以帮助我们很好地去理解这个东西,这样的话你再去看源码就会觉得轻松很多,毕竟底层原理是相同的,可能更多的是实现细节的优化。
因此,本文的学习重点是虚拟DOM以及DOM-Diff,那么我们就会实现一个简单的虚拟DOM和DOM-Diff。注意,本文的实现过程跟Vue或者React的实现不一定相同,是参考了网上的一些文章,以及个人的理解,是为了尽可能地帮助大家理解,所以实现过程可能不是最优。
二、对虚拟DOM的一些基础知识的理解
2.1 什么是虚拟DOM?
对于虚拟DOM可能很多没有了解过的人会觉得很高大上,不知道是个什么东西,事实上虚拟DOM是相对于真实DOM来进行阐述的。我们都知道一个真实的DOM就是我们常写的html,如下如所示:
<div id="test">
<p class = "item">节点1</p>
<span class = "item">节点2</span>
</div>
而虚拟DOM就是一个js对象,用来描述真实的DOM,它可能是长这样:
const vNode = {
tag:"div",//标签名或者组件名
data:{
id:"test",
},
children:[
{
tag:"p",data:{
className:"item"},children:"节点1"},
{
tag:"span",data:{
className:"item"},children:"节点2"}
]
}
我们可以看到上面通过一个vNode
对象来描述我们之前的真实DOM,这个对象就是虚拟DOM,对象里面的字段比如tag用来描述标签名称,data用来描述标签上的属性,children用来描述标签上的子元素。在Vue和React中不同的虚拟DOM,可能有不同的字段来描述,但是他们的功能都是为了描述真实DOM。也就是说虚拟DOM是一个对象,它是用来描述真实DOM。大家牢记这句话即可。
2.2为什么需要虚拟DOM?
很多人可能会奇怪,既然已经有了真实DOM,那么为什么还需要虚拟DOM了?万事万物,凡存在即合理。虚拟DOM的存在肯定是因为它带来了好处,相对应的也就是说真实DOM存在一些问题。这里我们参考网上一些常见的原因。
2.2.1 操作真实DOM性能开销大
如上图所示,我们可以看到一个简单的div元素,它身上的属性都非常庞大。当需要操作的DOM非常多,且操作非常频繁时,触发浏览器的渲染。由于浏览器的渲染涉及到:创建DOM树,创建CSSOM树,创建render树,布局和绘制这些流程。当频繁操作DOM时,会频繁地进行渲染,这样的话可能会带来性能和用户体验上的影响。因此,我们希望能够尽可能地去减少DOM的操作。而虚拟DOM的目的就是为了减少真实DOM的频繁操作。那么虚拟DOM就一定能够减少真实DOM的操作吗?或者换句话说虚拟DOM就一定会比真实DOM快吗?这是不一定的。比如说同样是创建10个元素,如果是真实DOM,直接创建10个元素,如果是虚拟DOM,最终还是需要渲染10个元素,反而还增加了虚拟DOM的创建时间,以及虚拟DOM-Diff时间,可能最终耗时反而比真实DOM更长。那么什么情况下使用虚拟DOM能够比使用真实DOM可能更快了。在以下两种情况下,虚拟DOM能够减少真实DOM的操作。
2.2.2 虚拟DOM减少DOM操作的两种情况
- 虚拟DOM合并多次操作 当我们需要进行多次DOM操作时,比如第一次修改样式宽度,第二次修改样式高度,第三次修改位置。如果是真实的DOM,那么每修改一次就会触发一次渲染,影响性能。但是虚拟DOM可以合并这几次操作,将所有的样式修改合并到一起修改,这样的话就相当于只修改了一次DOM,触发一次渲染。
- 虚拟DOM可以减少操作范围 当我们需要更新100个节点时,虚拟DOM可以通过DOM Diff发现只有10个需要更新,这样的话就只需要操作10个即可。从而减少操作DOM的范围。
2.3 虚拟DOM组成
在2.1节中,我们知道了虚拟DOM是一个对象,对象的一个一个属性用来描述虚拟DOM,而且不同的虚拟DOM其属性也不同,但是最终他们渲染出来的真实DOM确是一致的,这说明虚拟DOM有其固定的组成。我们看下React和Vue中虚拟DOM的组成,然后分析出他的一些必不可缺的组成部分。
React
const vNode = {
type:"div", // 标签名或者组件名
key:null,
props:{
// 属性
children:[ // 子元素或者子组件
{
type:"span",...},
{
type:"div",...},
],
className:"wrapper",
onClick:() => {}
},
ref:null,
...
}
Vue
const vNode = {
tag:"div", //标签名或者组件名
data:{
// 属性
class:"wrapper",
on:{
click:() => {}
}
},
children:[ // 子元素或者子组件
{
tag:"span",...},
{
tag:"div",...},
],
...
}
通过上面的对比,无论是React还是Vue中虚拟DOM的组成都肯定包含三个部分:
- type/tag:元素类型
- props/data:元素属性
- children:子元素集合
事实上,这也是描述一个真实DOM所不可或缺的三部分。type用来描述DOM的类型,是一个普通元素还是组件,
props用来描述这个元素身上的属性比如常见的style,class以及事件等,而children用来描述这个元素内部包裹的子元素。只有这三个都存在,才能够完整地描述一个元素。因此,我们知道了,一个虚拟DOM它至少应该是下面这种形式:
const vNode = {
tag:"div",
data:{
id:"test",
class:"item"
},
children:[
{
tag:"span",data:{},children:"span1"}
]
}
知道了虚拟DOM的具体组成,那么接下来我们就需要知道如何去创建虚拟DOM了。
三、虚拟DOM的创建和渲染
3.1 实现createElement函数创建虚拟DOM
通过上面的分析,我们已经知道了什么是虚拟DOM,为什么使用虚拟DOM以及虚拟DOM的组成。接下来我们就需要实现一个虚拟DOM。也就是说我们需要知道如何去生成虚拟DOM。事实上,创建虚拟DOM实际就是去创建一个如下的js对象。
{
tag:"div",
data:{
id:"test",
class:"item"
},
children:[
{
tag:"span",data:{},children:"span1"}
]
}
想要创建虚拟DOM,那么需要借助函数来实现,我们需要一个函数createElement
能够返回上面的数据,如下如所示:
function createElement(tag,data,children){
return {
tag,
data,
children
}
}
这是一个最简单的生成vNode的函数,我们使用这个函数来生成下面真实DOM的vNode。
<div id="test">
<p class = "item"></p>
</div>
通过createElement生成对应的vNode
var vNode = createElement("div",{
id:"test"},[
createElement("p",{
class:"item"},"p1")
])
console.log(JSON.stringify(str,null,2))
查看得到的结果是:
{
"tag": "div",
"data": {
"id": "test"
},
"children": [
{