前面我们用三小节介绍了compile
渲染template
,完成了 template --> AST --> render function
的过程。这篇我们主要介绍一下VNode
的生成过程,在此之前,我们先来了解一下一个基础的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
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = undefined
this.functionalOptions = undefined
this.functionalScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
包含了我们常用的一些标记节点的属性,为什么是VNode?其实网上已经有大量的资料可以了解到VNode的优势,比如大量副交互的性能优势,ssr的支持,跨平台Weex的支持...这里不再赘述。我们接下来看一下VNode的基本分类:
比如我们创建一个 emptyVNode
:
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
比如一个 textVNode
:
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
有了之前章节的知识,现在我们需要编译下面这段template
<div id="app">
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>
得到这样的render function
(function() {
with(this){
return _c('div',{ //创建一个 div 元素
attrs:{"id":"app"} //div 添加属性 id
},[
_m(0), //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render function
_v(" "), //空的文本节点
(message) //三元表达式,判断 message 是否存在
//如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
?_c('p',[_v("\n "+_s(message)+"\n ")])
//如果不存在,创建 p 元素,元素里面有文本,值为 No message.
:_c('p',[_v("\n No message.\n ")])
]
)
}
})
要看懂上面的 render function,只需要了解 _c,_m,_v,_s
这几个函数的定义,其中 _c 是 createElement
,_m 是 renderStatic
,_v 是 createTextVNode
,_s 是 toString
。
我们在编译的过程中调用了vm._render()
方法,其中_render
函数中有这样一句:
const { render, _parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement)
也就是通过调用自身的render
方法,传入$createElement
方法来生成VNode
节点。那么核心便在$createElement
的方法上了。
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode {
// 兼容传递参数顺序
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// 调用_createElement创建虚拟节点
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode {
/**
* 如果存在data.__ob__,说明data是被Observer观察的数据
* 不能用作虚拟节点的data
* 需要抛出警告,并返回一个空节点
*
* 被监控的data不能被用作vnode渲染的数据的原因是:
* data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
*/
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// 当通过 :is 动态设置组件时
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// 如果这里tag被设置成false或者不存在tag,创建一个空的节点。下面的注释应该是写错了...
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// 作用域插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据normalizationType的值,选择不同的处理方法
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 如果是保留的标签,处理...
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
// 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
// 当tag不是字符串的时候,我们认为tag是组件的构造类
// 所以直接创建
} else {
vnode = createComponent(tag, data, context, children)
}
// 经过上面的处理,如果能正常获得VNode,则返回,否则创建一个空的VNode
if (isDef(vnode)) {
if (ns) applyNS(vnode, ns)
return vnode
} else {
return createEmptyVNode()
}
}
经过上面的一系列过程,最终得到了我们的VNode,看起来是这样的:
render函数的执行,调用了createElement
方法,其内部通过传入的相关参数,根据不同类型,一步步解析出了VNode
。那么 VNode
如果进行渲染成不同平台所需的内容呢?下篇我们将会继续介绍patch
的工作。