VUE中的虚拟DOM
1.前言
虚拟DOM,这个名词作为当下的前端开发人员你一定不会陌生,至少会略有耳闻,但不会闻所未闻吧。大框架中关于虚拟DOM或多或少都有所涉及,然后接下来,我们就从原始码角度出发,看看Vue
中的虚拟DOM时怎样的。
#2.虚拟DOM简介
由于本系列文章是针对原始码深度Vue
学习的,所以着重分析在Vue
中对虚拟DOM是如何实现的,而对于虚拟DOM本身这个概念不做大篇幅的展开讨论,仅从以下几个问题简单介绍:
-
什么是虚拟DOM?
所谓虚拟DOM,就是用一个
JS
对象来描述一个DOM
节点,像如下示例:
<div class="a" id="b">我是内容</div>
{
tag:'div', // 元素标签
attrs:{ // 属性
class:'a',
id:'b'
},
text:'我是内容', // 文本内容
children:[] // 子元素
}
我们把其中DOM一个JS对象的必要东西通过一个对象表示出来,然后这个JS对象就可以使用描述这个DOM
异步,我们把这个JS对象就称为是这个真实DOM的虚拟DOM缓存。
-
为什么要有虚拟DOM?
我们知道,
Vue
是数据驱动视图的,数据发生变化视图就要随之更新,在更新视图的时候难免要操作DOM
,而操作真实DOM
又是非常耗费性能的,这是因为浏览器的标准就把DOM
设计的非常复杂,所以一个真正的DOM
元素是非常庞大的,如下所示:let div = document.createElement('div') let str = '' for (const key in div) { str += key + '' } console.log(str)
上图中我们打印一个简单的空
div
标签,就打印出这么多东西,更不用说复杂的,深层次的例程DOM
了。直接可见,直接操作真实DOM
是非常消耗性能的。
那么有没有什么解决方案呢?当然是有的。我们可以用JS
的计算性能来换取操作DOM
所消耗的性能。
既然我们逃不掉操作DOM
这道坎,但是我们可以重置少的操作DOM
。那如何在更新视图的时候重新少的操作DOM
呢?最直观的思路就是我们不要盲目的去更新视图,或者通过对比数据变化前后的状态,计算出视图中某个地方需要更新,只更新需要更新的地方,而不需要更新的地方则不需关心,这样我们就可以允许少的操作DOM
了。的用JS
的计算性能来换取操作DOM
的性能。
可以我们用JS
模拟出一个DOM
节点,虚拟称之为DOM
节点。当数据发生变化时,对比我们变化前后的虚拟DOM
节点,通过DOM-Diff
算法计算出需要更新的地方,然后去更新需要更新的视图。
这就是虚拟产生DOM
的原因以及最大的用途。
#3. Vue中的虚拟DOM
前文我们介绍了虚拟DOM
的概念以及为什么要有虚拟DOM
,那么在Vue
中虚拟DOM
是怎么实现的呢?然后,我们从源码出发,深入学习一下。
#3.1 VNode类
我们说了,虚拟DOM
就是用JS
来描述一个真实的DOM
节点而在。Vue
中就存在了一个VNode
类,通过这个类,就我们可以实例化出不同类型的虚拟DOM
节点,源码如下:
// 源码位置:src/core/vdom/vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag /*当前节点的标签名*/
this.data = data /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.children = children /*当前节点的子节点,是一个数组*/
this.text = text /*当前节点的文本*/
this.elm = elm /*当前虚拟节点对应的真实dom节点*/
this.ns = undefined /*当前节点的名字空间*/
this.context = context /*当前组件节点对应的Vue实例*/
this.fnContext = undefined /*函数式组件对应的Vue实例*/
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key /*节点的key属性,被当作节点的标志,用以优化*/
this.componentOptions = componentOptions /*组件的option选项*/
this.componentInstance = undefined /*当前节点对应的组件的实例*/
this.parent = undefined /*当前节点的父节点*/
this.raw = false /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.isStatic = false /*静态节点标志*/
this.isRootInsert = true /*是否作为跟节点插入*/
this.isComment = false /*是否为注释节点*/
this.isCloned = false /*是否为克隆节点*/
this.isOnce = false /*是否有v-once指令*/
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
get child (): Component | void {
return this.componentInstance
}
}
从上面的代码中可以研磨:VNode
类中包含了描述一个真实DOM
例程所需要的各种属性,如tag
表示解码器的标签名text
,children
表示该中间包含的文本,表示该包含包含的子例程等。间不同的搭配,就可以描述出各种类型的真实摘要DOM
。
#3.2 VNode的类型
上一小节最后我们说了,通过属性之间不同的搭配,VNode
类可以描述出各种类型的真实基准DOM
。那么它都可以描述出某种类型的例程呢?通过阅读代码,可以发现通过不同属性的搭配,可以描述出以下几种类型的例程。
- 注释注释
- 文字摘要
- 元素推理
- 组件例程
- 函数式组件例程
- 克隆议员
接下来,我们就把这几种类型的例程描述方式从二进制中一一对应起来。
#3.2.1注释注释
注释描述符描述起来相对就非常简单了,它只是两个属性就够了,源码如下:
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
从上面的代码中可以看到,描述一个注释上面的两个属性,分别是:text
和isComment
。其中text
属性表示具体的注释信息,isComment
是一个标志,用于标识一个队列是否是注释注释。
#3.2.2文本基线
文本摘要描述起来比注释注释更简单,因为它只需要一个属性,那就是text
属性,表示特定的文本信息。
// 创建文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
#3.2.3克隆例程
克隆副本就是把一个已经存在的副本复制副本出来,它主要是为了做模板编译优化时使用,这个后面我们会说到。
// 创建克隆节点
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
从上面的代码中可以看到,克隆指针就是把已有的属性的全部复制到新例程中,而现有例程和新克隆得到的例程之间唯一的不同就是克隆得到的例程isCloned
为true
。
#3.2.4元素例程
在元素之下,元素例程更贴近于我们通常看到的真实DOM
例程,它具有描述例程标签名词的tag
属性,描述例程属性如class
,attributes
等的data
属性,具有描述包含的子例程信息的children
属性等。所包含的情况相对而言比较复杂,源码中没有像前三种相同的直接写死(当然也不可能写死),那就举个简单的例子说明一下:
// 真实DOM节点
<div id='a'><span>难凉热血</span></div>
// VNode节点
{
tag:'div',
data:{},
children:[
{
tag:'span',
text:'难凉热血'
}
]
}
我们可以看到,真实例程DOM
中:div
标签里面包含了一个span
标签,而span
标签里面有一段文本。反应到VNode
例程上就如上所示:tag
表示标签名,data
表示标签的属性id
等,children
表示子数组。
#3.2.5组件例程
组件上游除有元素索引具有的属性之外,它还有两个特有的属性:
- componentOptions:组件的选项,如组件的
props
等 - componentInstance:当前组件例程对应的
Vue
实例
#3.2.6函数式组件例程
函数式组件例程相较于组件例程,它又有两个特有的属性:
- fnContext:函数式组件对应的Vue实例
- fnOptions:组件的选项选项
#3.2.7小结
以上就是VNode
可以描述的多种多样的类型,它们本质上都是VNode
类的实例,只是在实例化的时候引发的属性参数不同而已。
#3.3 VNode的作用
说了这么多,那么VNode
在Vue
的整个虚拟DOM
过程起了什么作用呢?
其实VNode
是作用是相当大的。我们在视图渲染之前,把写好的template
模板先编译成VNode
并缓存下来,等到数据发生变化页面需要重新渲染的时候,我们把数据发生变化后生成的VNode
与前一次缓存下来的VNode
进行对比,发现差异,然后有差异的对应VNode
的真实DOM
前缀就是需要重新渲染的例程,最后根据有差异的VNode
创建出真实的DOM
变量再插入到视图中,最终完成一次视图更新。
#4.总结
本章首先介绍了虚拟DOM
的一些基本概念和为什么要有虚拟DOM
,其实说白了就是以JS
的计算性能来换取操作真实DOM
所消耗的性能。然后从原始码角度我们知道了在Vue
中是通过VNode
类来实例化出不同类型的虚拟DOM
例程,并且学习了不同类型生成的属性的不同,所谓的不同类型的转换器其本质还是一样的,都是VNode
类的实例,只是在实例化时替代的属性参数不同罢了。最后探究了VNode
的作用,有了数据变化前后的VNode
,我们才能进行后续的发现DOM-Diff
差异,最终做到只更新有差异的观点,从而达到减少较少的操作真实DOM
的目的,以节省性能。