什么是VNode
在Vue中存在一个VNode类,使用它可以实例化不同类型的vnode实例,而不同类型的vnode实例各自表示不同类型的DOM元素。
例如,DOM元素有元素节点、文本节点和注释节点,vnode实例也会对应着有元素节点、文本节点和注释节点等。
VNode类的代码如下:
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只是一个名字,本质上其实是JS中一个普通的对象,是从VNode类实例化的对象。我们用这个JS对象来描述一个真实的DOM元素的话,那么该DOM元素上的所有属性在VNode这个对象上都存在对应的属性。
简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。也就是说,我们可以把vnode理解成JS对象版本的DOM元素。
VNode的作用
由于每次渲染视图时都是先创建vnode,然后使用它创建真实DOM插入到页面中,所以可将上一次渲染视图时所创建的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode和上一次缓存的vnode进行对比,查看他们之间有哪些不一样的地方,找出这些不一样的地方并基于此去修改真实的DOM。
Vue目前对状态的侦测策略采用了中等粒度,当状态发生变化时,只通知到组件级别,然后组件内使用虚拟DOM来渲染视图。
也就是说,只要组件使用的众多状态中有一个发生了变化,那么整个组件就要重新渲染。
如果组件只有一个节点发生了变化,那么重新渲染整个组件的所有节点,很明显会造成大的性能浪费。因此,对vnode进行缓存,并将上一次缓存的vnode和当前新创建的vnode进行对比,只更新发生变化的节点就变得尤为重要。这也是vnode最重要的一个作用。
VNode类型
通过阅读源码,可以发现通过不同属性的搭配,可以描述出以下几种类型的节点。
- 注释节点
- 文本节点
- 元素节点
- 组件节点
- 函数式组件节点
- 克隆节点
接下来,我们就把这几种类型的节点描述方式从源码中一一对应起来。
注释节点
注释节点描述起来相对就非常简单了,它只需两个属性就够了,源码如下:
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
代码中可以看到,描述一个注释节点只需两个属性,分别是:text和isComment。其中text属性表示具体的注释信息,isComment是一个标志,用来标识一个节点是否是注释节点。
文本节点
文本节点描述起来比注释节点更简单,因为它只需要一个属性,那就是text属性,用来表示具体的文本信息。源码如下:
// 创建文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
克隆节点
克隆节点就是把一个已经存在的节点复制一份出来,它主要是为了做模板编译优化时使用,这个后面我们会说到。关于克隆节点的描述,源码如下:
// 创建克隆节点
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。
元素节点
相比之下,元素节点更贴近于我们通常看到的真实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表示子节点数组。
组件节点
组件节点除了有元素节点具有的属性之外,它还有两个特有的属性:
- componentOptions :组件的option选项,如组件的
props等 - componentInstance :当前组件节点对应的
Vue实例
函数式组件节点
函数式组件节点相较于组件节点,它又有两个特有的属性:
- fnContext:函数式组件对应的Vue实例
- fnOptions: 组件的option选项
小结
VNode是一个类,可以生成不同类型的vnode实例,而不同类型的vnode表示不同类型的真实DOM元素。
由于Vue对组件采用了虚拟DOM来更新视图,当属性发生变化时,整个组件都要进行重新渲染操作,但组件内并不是所有DOM节点都需要更新,所以将vnode缓存并将当前新生成的vnode和上一次缓存的oldVnode进行对比,只对需要更新的部分进行DOM操作可以提升很多性能。
vnode有很多类型,它们本质上都是从VNode类实例化出的对象,其唯一区别只是属性不同。