先来看下vnode什么样
VNode构造函数
(源码目录:core/vdom/vnode.js)
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 //节点数据包括class, style,attrs等
this.children = children //子节点
this.text = text //文本
this.elm = elm //真实dom
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key //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用来描述DOM节点的,包括tag, data, text, children, elm, parent等。
data属性主要保存一些节点数据,比如style,class,attrs等。
vnode对象 由render function运行生成,下面先从 render 函数的创建开始说
两种生成render函数的方式:
-
第一种是用户在Vue对象中直接创建 render function。
-
第二种是 Vue 编译模板生成 render function。详细过程请看这篇文章:template编译成render function
用户自己写 render 函数和 vue 编译生成的差不多,下面看下第一种方式:
<div id="app"></div>
<script type="text/javascript">
const vm = new Vue({
el: '#app',
render(h) {
return h('div', {
attrs:{
id: 'app'
}
}, [ h('h1', [' hello ' + this.name]) ])
},
data: {
name: 'ludeng',
}
});
</script>
如上,render函数有一个参数h,这个参数h是用来创建vnode虚拟节点的函数。h函数接收三个参数,h( tag,data| Object,children| String/Array ),分别对应 标签名,数据(class, style, attrs等),子级vnode。
data,和 children 都是可选的,另外 children 可以是 Array 或 String 类型,内容只有文本可以用 String 类型。
接下来,我们通过一小段代码(第二种方式)看编译生成的渲染函数:
vue模板编译示例
<div id="app">
<h1> hello {{ name }}</h1>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#app',
data: {
name: 'ludeng',
}
});
</script>
在控制台,通过vm.$options.render打印出渲染函数:
function anonymous() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('h1', [_v(" hello " + _s(name))])])
}
}
函数内使用了with方法,with(this)里的属性和方法,相当于通过this来调用,这样写是为了减少代码量,这里this指向Vue实例。(比如:_c 实际调用 vm._c(), name 实际访问 vm.name )
与用户写的render function不同是有了_c, _v, _s等方法,这些都是创建vnode相关的方法。源码定义如下:
_c _v _s
(源码目录:/core/instance/render.js)
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) //a, b, c 分别对应 tag, data, children, d 是辅助变量
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) //第一种方式的 *h函数!
(源码目录:core/instance/render-helpers/index.js)
Vue.prototype._s = toString //将值转换为字符串
Vue.prototype._v = createTextVNode //创建 text vnode
下面根据示例运行顺序进行讲解:
( *** 在此之前说明一点,_c函数并不会先运行,这是传参的方式调用函数,_s才是第一个运行的函数)
(1) render函数运行,先访问name,然后调用_s(name)转化成String类型,与" hello "进行字符串拼接。
(2) 调用 _v ( createTextVNode ) 函数创建 text vnode
看下createTextVNode函数:
(源码目录:core/vdom/vnode.js)
function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
text vnode创建很简单,只传入一个字符串。
(3) _v 运行完,调用 _c 继续创建 vnode
_c 和 h(第一种方式) 这俩函数都会调用 createElement 方法,下面看 createElement方法:
createElement
(源码目录:core/vdom/create-element.js)
function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
//data是数组 或者 原始值(string number symbol boolean),此时参数不对应
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
//用户写的render函数这里为true(_c: false, h: true)
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
createElement方法又调用了_createElement方法。多此一举的嵌套调用主要是为了让接口更灵活。比如:
_c('h1', [ _v(" hello " + _s(name)) ])
_c 没有 data 参数,造成下面参数不对应现象:
tag:'h1',
data: [ _v(" hello " + _s(name)) ]
if判断进行调整,调整后:
tag:'h1',
data:undefined,
children:[ _v(" hello " + _s(name)) ]
有了调整这个步骤,使得参数设置更灵活,尤其是用户自己写render函数的时候。
_createElement函数才是真正创建 vnode 对象的函数,下面来看这个函数:
_createElement
(源码目录:core/vdom/create-element.js)
function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<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()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 标准化 children
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)) {
//平台内置元素: 比如 html, div, h1等标签
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
vnode = new VNode(
config.parsePlatformTagName(tag), //web平台 直接返回tag
data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 通过 vue 注册的自定义 component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {// vnode != null
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
_createElement创建VNode对象,如果是 reserved tag(比如 html, div, h1等标签)则创建普通 VNode 对象,如果是component tag(通过 Vue 注册的自定义 component), 则会创建 Component VNode 对象。
其实,主要都是根据 tag, data, children 这三个数据创建。
最后,在控制台打印vm._vnode,看下虚拟DOM树