所谓虚拟DOM,就是用一个JS对象来描述一个DOM节点,如下:
ts
<div class="a" id="b">我是内容</div>
{
tag:'div', // 元素标签
attrs:{ // 属性
class:'a',
id:'b'
},
text:'我是内容', // 文本内容
children:[] // 子元素
}
把组成一个DOM节点的必要东西通过一个JS对象表示出来,那么这个JS对象就可以用来描述这个DOM节点,我们把这个对象成为真实DOM节点的虚拟DOM节点
那么为什么需要这个虚拟DOM节点?我们知道VUE是数据驱动视图的,数据变化视图随之变化,在更新视图时需要操作DOM,而操作真实DOM节点是非常耗费性能的,这是因为浏览器把DOM节点设计的非常复杂,一个真实DOM节点是非常庞大的。
所以对于需要时常更新的视图我们不能每次都去操作真实DOM,这是不合理的。所以VUE提出了解决办法,就是用JS的计算性能换取操作DOM消耗的性能。
既然我们逃不掉操作DOM,那么我们可以尽可能的减少操作DOM。那如何在更新视图时尽可能少的操作DOM呢?最直观的思路就是我们不要盲目的去更新DOM,而通过对比更新前后的状态,计算哪些地方需要更新而哪些地方没有变化,以此去操作只需要更新的地方,其他地方原封不动,这样便可以尽可能少的操作DOM。也就是用JS性能换取DOM消耗性能。
我们可以用JS模拟出一个DOM节点,称之为虚拟DOM节点。当数据发生变化时,我们将变化前后的虚拟DOM节点进行对比,通过diff算法计算出需要更新的地方,然后去更新需要更新的视图。这就是虚拟DOM节点的意义。
VUE的虚拟DOM
vue中存在一个VNode类,通过这个类,可以实例化出不同类型的虚拟DOM节点,源码如下:
TS
// 源码位置: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节点。
1.VNode的类型
VNode可以描述出不同类型的真实DOM节点,那么有哪些类型呢?
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件节点
- 克隆节点
1.1 注释节点
ts
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
描述一个注释节点只需要两个属性:text和isComment。其中text属性表示具体的注释信息,isComment是一个标志,用来标识一个节点是否是注释节点。
1.2文本节点
TS
// 创建文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
只有一个text属性
1.3克隆节点
TS
// 创建克隆节点
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
}
就是把一个已经存在的节点复制一份出来,主要为了做模版编译优化使用
1.4元素节点
TS
// 真实DOM节点
<div id='a'><span>ww</span></div>
// VNode节点
{
tag:'div',
data:{},
children:[
{
tag:'span',
text:'ww'
}
]
}
元素节点更贴近于我们看到的真实dom节点,它有描述标签的tag属性,描述节点属性如class,attributes等的data属性,有描述子节点信息的chilrend属性。
1.5组件节点
除了有元素节点具有的属性外,还有两个特有属性:
componentOptions:组件的option选项,如组件的props等
componentInstance:当前组件节点对应的Vue实例
1.6函数式组件节点
同上
fnContext:函数式组件对应的vue实例
fnOptions:组件的option选项
2.VNode的作用
我们在视图渲染之前,把写好的template模版编译成VNode并缓存下来,等到数据更新时页面需要重新渲染的时候,我们把数据变化后的VNode节点与前一次缓存的进行对比,找出差异,最后有差异的VNode对应的真实DOM就是需要重新渲染的节点,根据有差异的VNode创建出真实DOM插入到视图中,完成一次视图更新。