Vue 组件 render
# VNode
export default class VNode {
tag: string | void
data: VNodeData | void
children: ?Array<VNode>
text: string | void
elm: Node | void
ns: string | void
context: Component | void // rendered in this component's scope
key: string | number | void
componentOptions: VNodeComponentOptions | void
componentInstance: Component | void // component instance
parent: VNode | void // component placeholder node
// strictly internal
raw: boolean // contains raw HTML? (server only)
isStatic: boolean // hoisted static node
isRootInsert: boolean // necessary for enter transition check
isComment: boolean // empty comment placeholder?
isCloned: boolean // is a cloned node?
isOnce: boolean // is a v-once node?
asyncFactory: Function | void // async component factory function
asyncMeta: Object | void
isAsyncPlaceholder: boolean
ssrContext: Object | void
fnContext: Component | void // real context vm for functional nodes
fnOptions: ?ComponentOptions // for SSR caching
devtoolsMeta: ?Object // used to store functional render context for devtools
fnScopeId: ?string // functional scope id support
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.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = 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
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom (opens new window) 的实现。
Vnode 定义的属性差不多有 20 几个,显然用 Vnode 对象要比真实 DOM 对象描述的内容要简单得多,它只用来单纯描述节点的关键属性,例如标签名,数据,子节点等。并没有保留跟浏览器相关的 DOM 方法。除此之外,Vnode 也会有其他的属性用来扩展 Vue 的灵活性。
# 为什么使用 Virtual DOM?
性能方面考量
- 创建真实 DOM 的代价高:真实的 DOM 节点 node 实现的属性很多,而 vnode 仅仅实现一些必要的属性,相比起来,创建一个 vnode 的成本比较低
- 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升,引入虚拟 DOM 可以简化 DOM 的复杂操作。其实为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM,Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
- 通过比较前后两次状态的差异实现真实 DOM 的最小化更新,这样可以减少浏览器的重绘及回流:使用 vnode 相当于加了一个缓冲,让一次数据变动所带来的所有 node 变化,先在 vnode 中进行修改,然后 diff 之后对所有产生差异的节点集中一次对 DOM tree 进行修改,以减少浏览器的重绘及回流。
性能方面的考量仅仅是一方面,性能受场景的影响是非常大的,不同的场景可能造成不同实现方案之间成倍的性能差距,所以依赖细粒度绑定及 Virtual DOM 哪个的性能更好还真不是一个容易下定论的问题
- Vue 之所以引入了 Virtual DOM,更重要的原因是为了
解耦 HTML 依赖
,这带来两个非常重要的好处是:- 不再依赖 HTML 解析器进行模版解析,可以进行更多的 AOT 工作提高运行时效率:通过模版 AOT 编译,Vue 的运行时体积可以进一步压缩,运行时效率可以进一步提升;
- 可以渲染到 DOM 以外的平台,实现 SSR、同构渲染这些高级特性,Weex 等框架应用的就是这一特性。
# 生成 VNode
# Vue.prototype._render
去掉部分代码,只保留关键逻辑。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
vm.$vnode = _parentVnode
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
vnode = vm._vnode
} finally {
currentRenderingInstance = null
}
// ...
// 设置 parent
vnode.parent = _parentVnode
return vnode
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这样看下来,_render
方法的内部实现就很简单,主要就是通过调用 vm.$options.render
生成这个组件的 VNode 并返回。vm.$options.render
方法可以是用户手写的 render 也可以是 模板通过编译生成的 render。下面就通过一个例子来看下这两种情况下的 render 函数
# 一个例子
这里我们采用 compiler
版本的 Vue 来构建项目。
- App.vue
<template>
<div class="content-wrapper">{{ name }}</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
name: 'App',
}
},
created() {
console.log('this is App.vue')
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- main.js
import App from './App.vue'
new Vue({
components: { App }
data: {
name: 'main',
},
created() {
console.log('this is main.js')
},
template: '<div id="app"><App /></div>',
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
# template 运行时编译
由于提供的是 template,所以 vue 实例在挂载过程(运行时)中,会将 template 编译成如下形式的 render 函数,
function anonymous() {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [_c('App')], 1)
}
}
2
3
4
5
# App 组件构建时编译
组件 App.vue 则会在项目构建时被 vue-loader
处理成如下形式的一个组件对象,
{
name: "App",
data: function data() {
return {
name: 'App'
};
},
created: function created() {
console.log('this is App.vue');
},
render: function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "content-wrapper" }, [
_vm._v(_vm._s(_vm.name))
])
},
_compiled: true,
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- App 组件的模板部分也被编译成了
render
函数,这个是通过vue-template-compiler
处理后生成的。 - 而且通过
vue-template-compiler
编译生成的render
函数的内部调用的都是_c
函数,而不是用户手写render
时传入的h
函数
# h
与 _c
无论是 h
函数还是 _c
函数,它们的内部都调用了 createElement
方法,二者的差别,仅在调用 createElement 时最后一个参数alwaysNormalize
不同。
这个参数的含义是:是否要对传入的组件对象进行规范化。用户手写 render 的情况,是需要进行规范化的
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = function (a, b, c, d) {
return createElement(vm, a, b, c, d, true)
}
2
3
4
5
回到 render 函数的调用逻辑上来,vue 实例的 render 会首先被调用,
function anonymous() {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [_c('App')], 1)
}
}
2
3
4
5
外层的 _c
方法执行之前,会先执行内部的 _c('App')
,所以子组件 App
的 VNode 会先被创建
# createElement
function createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
该方法内部对参数做了一些处理之后,最终又调用了_createElement
# _createElement
该方法就是真正生成 VNode 的方法。有关子组件的规范化逻辑,文章后面会提到,_c('App')
的逻辑中尚不涉及,所以先跳过
// prettier-ignore
function _createElement(context, tag, data, children, normalizationType) {
// ========= 数据对象不能是响应式数据 =========== //
if (isDef(data) && isDef(data.__ob__)) {
// 返回注释节点
return createEmptyVNode()
}
// =========== 处理动态组件 =========== //
if (isDef(data) && isDef(data.is)) {
// 如果是动态组件就会读取组件的 is 属性,作为当前的 tag
tag = data.is
}
if (!tag) {
// 防止动态组件 :is 属性设置为false
return createEmptyVNode()
}
// =========== 支持单个函数式子组件作为默认作用域插槽 ============ //
if (Array.isArray(children) && typeof children[0] === 'function') {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// ============== 子组件的规范化 ================= //
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// ============== 不同类型标签的处理 =============== //
let vnode, ns
// 一. 标签为字符串类型,比如 div、App 这种组件占位符
if (typeof tag === 'string') {
let Ctor
// 1. 如果是平台内置的节点类型,如 div
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
// 2. 如果是已注册的 组件 类型
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 3.其他情况
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// 二. 组件对象的场景,比如:
/**
* import App from './App'
*
* // ...
*
* render(h) {
* h(App)
* }
* */
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// ...省略
if (isDef(data)) {
registerDeepBindings(data)
}
return vnode
} else {
return createEmptyVNode()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
上面说到 _c('App')
会先被执行,此时传递给_createElement
的参数如下:
- context:
vue 实例
- tag:
'App'
- data:
undefined
- children:
undefined
- normalizationType:
undefined
此时,'App'
是一个 string
类型的组件占位符 tag,而且它不是平台内置的,并且它已经在 vue 实例的$options.components
中注册, 所以 vnode 的创建会走到 2. 如果是已注册的 组件 类型
那里
vnode = createComponent(Ctor, data, context, children, tag)
传给createComponent
的参数如下:
- Ctor
即上面的 App 组件对象
{
name: "App",
data: function data() {
return {
name: 'App'
};
},
created: function created() {
console.log('this is App.vue');
},
render: function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", { staticClass: "content-wrapper" }, [
_vm._v(_vm._s(_vm.name))
])
},
_compiled: true,
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
context:vue 实例
tag:'App'
data & children:
undefined
接下来就看下 createComponent
方法的实现,
# createComponent
function createComponent(Ctor, data, context, children, tag) {
if (isUndef(Ctor)) {
return
}
// ======== 1. 基于 Vue 构造器创建一个子类构造器 ========= //
var baseCtor = context.$options._base
if (isObject(Ctor)) {
// 这里就相当于 Ctor = Vue.extend(App)
Ctor = baseCtor.extend(Ctor)
}
// ============== 异步组件相关 =============== //
var asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}
// 组件数据
data = data || {}
// ============= 2. 获取构造器上的配置对象,并完成配置合并操作 ========== //
resolveConstructorOptions(Ctor)
// 处理 v-model
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// 处理 props
var propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ============== 函数式组件相关 =============== //
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// 处理 event
var listeners = data.on
data.on = data.nativeOn
// =============== 抽象组件相关 =============== //
if (isTrue(Ctor.options.abstract)) {
var slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// =============== 3. 安装组件钩子函数 =============== //
installComponentHooks(data)
// 如果组件对象存在 name 属性,则组件名称以 name 为准
var name = Ctor.options.name || tag
// =============== 4. 创建组件 VNode =============== //
var vnode = new VNode(
'vue-component-' + Ctor.cid + (name ? '-' + name : ''),
data,
undefined,
undefined,
undefined,
context,
{
Ctor: Ctor,
propsData: propsData,
listeners: listeners,
tag: tag,
children: children,
},
asyncFactory
)
// 返回组件 VNode 实例
return vnode
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
整个流程比较复杂,分支逻辑比较多,这里只说一下涉及到的处理逻辑:
- 创建子组件构造器
通过 Ctor = baseCtor.extend(Ctor)
,让子组件继承 Vue,并返回一个子组件的构造器
- 完成子组件的配置合并
通过 resolveConstructorOptions(Ctor);
- 为 App 这个组件对象安装一些钩子函数,这些钩子都是在后续的
patch
过程中会被调用。
最后,看下 App 组件最终生成的 VNode 究竟长什么样子:
{
tag: "vue-component-1-App",
data: {
hook: {
init: function init (vnode, hydrating) {
// ...
},
prepatch: function prepatch (oldVnode, vnode) {
// ...
},
insert: function insert (vnode) {
// ...
},
destroy: function destroy (vnode) {
// ...
},
},
},
// ...
componentOptions: {
Ctor: function VueComponent (options) {
this._init(options);
},
propsData: undefined,
listeners: undefined,
tag: "App",
children: undefined,
},
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这里可以重点关注一下 data.hook
以及 componentOptions
两个属性,因为后面的 patch 过程中会涉及到
# 子组件的规范化
说完了 App 组件的 VNode 创建过程,现在再次回到 vue 实例的 render 方法中来,此时的 _c('App')
已经完成构建,返回了上面的 VNode 对象,这里我们以 VApp
来代表
function anonymous() {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [VApp], 1)
}
}
2
3
4
5
此时传递给 _createElement
方法的参数就成了:
- context:
vue 实例
- tag:
'div'
- data:
{ attrs: { id: 'app' } }
- children:
[VApp]
- normalizationType:
1
因为此时传递的 children 属性不为空,所以就会涉及到 children 的规范化操作,规范化的目的就是要将 children 里的所有元素都标准化为 VNode 类型
if (normalizationType === 2) {
children = normalizeChildren(children)
} else if (normalizationType === 1) {
children = simpleNormalizeChildren(children)
}
2
3
4
5
# normalizeChildren
// prettier-ignore
function normalizeChildren(children) {
// 递归调用,直到子节点是基础类型,就创建文本节点Vnode
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
// 判断是否基础类型
function isPrimitive(value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个方法的调用场景分为两种:
# 第一种
是用于处理模板中包含 template
、slot
、v-for
的情况,这种情况下会产生嵌套数组,此时会调用 normalizeArrayChildren
方法递归处理 children
举个例子
new Vue({
el: '#app',
components: { App },
template: `<div id="app"><template v-for="i in 2"><App/></template></div>`,
data: {
name: 'main.js',
},
created() {
console.log('this is main.js')
},
})
// ======= template 编译后生成的 render 函数 ======== //
function anonymous() {
with (this) {
return _c(
'div',
{ attrs: { id: 'app' } },
[
_l(2, function (i) {
return [_c('App')]
}),
],
2
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这种情况下,_createElement
方法的 children 参数就会是如下的形式:
;[
[
[
{
tag: 'vue-component-1-App',
// ...
},
],
[
{
tag: 'vue-component-1-App',
// ...
},
],
],
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可以看到这是一个嵌套了三层的数组,并且在第二层数组对象上还会有一个 _isVList: true
的属性,这个属性是与数组的 length 属性平级的。
这种情况下,经过规范化处理之后,会变成如下的形式,被拍成了只有一层的数组
[
{
tag: 'vue-component-1-App',
// ...
},
{
tag: 'vue-component-1-App',
// ...
}
],
2
3
4
5
6
7
8
9
10
# 第二种
是用于处理 用户手写 render 函数
的情况,就不举例子了。
# simpleNormalizeChildren
处理编译生成的 render 函数。理论上 template 模板通过编译生成的 render 函数都是 Vnode 类型,但是有一个例外,函数式组件返回的是一个数组,这个时候处理结果依然是将整个 children 拍平成一维数组。
function simpleNormalizeChildren(children) {
for (var i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
2
3
4
5
6
7
8
不再举例。
# 总结
先用一个例子来总结一下:
- HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
msg: 'HelloWorld',
}
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- App.vue
<template>
<div class="content-wrapper">
<HelloWorld />
{{ name }}
</div>
</template>
<script>
import HelloWorld from './HelloWorld.vue'
export default {
name: 'App',
components: { HelloWorld },
data() {
return {
name: 'App',
}
},
created() {
console.log('this is App.vue')
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- main.js
import App from './HelloWorld.vue'
new Vue({
el: '#app',
components: { App },
template: `<div id="app"><App/></div>`,
data: {
name: 'main.js',
},
created() {
console.log('this is main.js')
},
})
2
3
4
5
6
7
8
9
10
11
12
上面这个例子最终构建出来的 VNode 如下:
{
tag: "div",
data: {
attrs: {
id: "app",
},
},
children: [
{
tag: "vue-component-1-App",
data: {
on: undefined,
hook: {
init: function init (vnode, hydrating) {
// ...
},
prepatch: function prepatch (oldVnode, vnode) {
// ...
},
insert: function insert (vnode) {
// ...
},
destroy: function destroy (vnode) {
// ...
},
},
},
componentOptions: {
Ctor: function VueComponent (options) {
this._init(options);
},
tag: "App",
},
componentInstance: undefined,
},
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
可以看到,生成的 VNode 里并没有 HelloWorld 组件相关的内容,至于为什么,在下一篇 patch 过程的时候再做说明。
最后,用一张图来总结下 组件 VNode 的创建过程:
另外,记住一点,如果某个组件内部引入了其他组件的话,那子组件 VNode 的创建是优先于父组件的