Vue源码-组件化

组件化是Vue的核心概念,它让我们提取可复用的模版和脚本,在需要用到的地方插入对应的组件标签。那Vue是怎么把组件渲染成真实的DOM的呢,它和我们渲染普通的HTML有啥区别?我们在上篇文章中实例插入一个组件,通过源码分析组件怎么渲染?

组件虚拟节点

我们通过new Vue()插入一个组件节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// App.vue
<template>
<div class="app">
{{ msg }}
</div>
</template>

<script>
export default {
name: 'app',
data() {
return {
msg: 'Hello Vue'
};
}
};
</script>

在入口文件挂载:

1
2
3
4
5
6
7
import Vue from 'vue';
import App from './App.vue';

new Vue({
el: '#app',
render: h => h(App)
});

从上篇分析new Vue()的主流程后,我们知道vue实例会调用vm.$mount()进行挂载。然后新建一个实例的渲染watcher,并执行下面的updateComponent函数:

1
2
3
updateComponent = () => {
vm._update(vm._render(), hydrating)
}

该函数先调用vm._render()生成一个虚拟节点,很明显这是一个App组件节点。所以我们来看看_createElement()对组件节点是怎么创建的。它定义在src/core/vdom/create-element.js

1
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
if (typeof tag === 'string') {
let Ctor
// 获取tag的命名空间
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 平台保留的标签
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), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 在vm的options的components选项查找,如果存在则是组件节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知的元素
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}

上面是创建虚拟节点的关键代码。因为tag是一个App组件的配置对象,所以调用createComponent()方法生成一个组件虚拟节点。这个方法定义在src/core/vdom/create-component.js:

1
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
82
83
84
85
86
87
88
89
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}

// _base的值为Vue构造函数
const baseCtor = context.$options._base

// 组件的配置项是对象,调用Vue.extend生成vue组件类
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}

// 异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 异步组件一开始用空的注释节点做占位
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

data = data || {}

resolveConstructorOptions(Ctor)

// transform component v-model data into props & events
// v-model的处理
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}

// 从vnode的data中提取props数据
const propsData = extractPropsFromVNodeData(data, Ctor, tag)

// functional component
// 函数组件处理
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}

const listeners = data.on
data.on = data.nativeOn

if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

// install component management hooks onto the placeholder node
// 安装组件vnode的各种钩子
installComponentHooks(data)
// 返回组件的占位节点
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)

// ..

return vnode
}

这个方法一开始先获取context.$options._base,也就是Vue构造函数。然后调用Vue.extend()返回组件的构造器。它定义在src/core/global-api/extend.js

1
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
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid

// 缓存组件的构造函数到组件配置对象
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}

const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}

const Sub = function VueComponent (options) {
this._init(options)
}
// 继承Vue原型的方法
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++

// 合并配置,组件构造器也拥有Vue.options全局配置
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super

// 把组件props和computed挂载原型上,避免每次实例话组件的时候
// 都在实例上代理一遍
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}

// 各种Vue静态方法的继承
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}

Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)

cachedCtors[SuperId] = Sub
return Sub
}

这个方法一开始先缓存组件构造器到组件配置对象的extendOptions._Ctor中,在后面再用到该组件,直接返回组件构造器。然后定义组件构造器:

1
2
3
const Sub = function VueComponent (options) {
this._init(options)
}

所以新建一个组件实际入口也是vm._init()方法。然后指定原型为Vue.prototype让它拥有Vue原型方法。接下来处理组件的option配置,它调用mergeOptions()方法把Vue.options上的属性合并到组件构造器的options,比如平时我们全局注册的组件,就是在这步进行合并,让我们组件内直接使用而不用声明。最后,把Vue上面的静态方法也赋值到组件构造器,最后返回这个构造器。

回到createComponent()方法,如果创建组件配置不是一个对象,证明是一个异步组件,所以下面就是对异步工厂函数的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// 异步组件一开始用空的注释节点做占位
// return a placeholder node for async component, which is rendered
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

里面的实现细节我们之后再分析。接下来是处理v-modelprops等,我们暂且跳过。然后到了下面这个方法:

1
installComponentHooks(data)  // 安装钩子

这个方法主要是安装组件vnode在接下来patch过程需要调用的钩子函数。它的定义:

1
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
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 创建组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance // 组件占位符所在的vm
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},

prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},

insert (vnode: MountedComponentVNode) {
// ...
},

destroy (vnode: MountedComponentVNode) {
// ..
}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

// 合并对应阶段的钩子
function mergeHook (f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}

function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}

显而易见,它把componentVNodeHooks对象定义的各种阶段钩子合并到了我们新建的组件vnode的hook属性上面。至此我们组件vnode就创建完成并返回,输出下返回的结果:

创建组件实例

现在我们已经得到组件的虚拟DOM,正确的说是组件占位vnode。因为它在渲染后会被组件对应的DOM替换,比如我们例子的<App/>组件。

执行完render后调用Vue实例的vm._update()方法,该方法里面会走下面的方法:

1
2
3
4
5
if (!prevVnode) {
// initial render
// 首次挂载
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}

接下来的过程上一篇一样。会进行patch()方法并执行oldvnode为真实DOM的条件逻辑,不同的是createElm()方法里面会执行到下面代码为true并返回,因为我们此时vnode为组件虚拟节点:

1
2
3
4
// 组件节点的处理
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

来看下createComponent方法的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建组件包括组件的实例,组件的挂载
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data;
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef((i = i.hook)) && isDef((i = i.init))) {
// 调用组件vnode的init钩子
i(vnode, false /* hydrating */);
}

if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
// 把组件对应的DOM替换组件占位符
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}

这个方法会先调用vnode上面的init钩子函数,init钩子主要是创建组件实例,并且得到组件真实DOM节点。我们看下init钩子的定义。在定义我们前面创建组件vnode的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 创建组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance // 组件占位符所在的vm
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},

//...
}

这个方法createComponentInstanceForVnode创建组件的实例。再看这个方法实现之前,现在说下activeInstance变量的意义。它是当前Vue或者组件实例,作为一个全局变量,主要维护组件树对应的实例对象的父子关系。在vm._update()方法里面处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode

if (!prevVnode) {
// initial render
// 首次挂载
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()

// ..
}

setActiveInstance()方法定义:

1
2
3
4
5
6
7
export function setActiveInstance(vm: Component) {
const prevActiveInstance = activeInstance
activeInstance = vm
return () => {
activeInstance = prevActiveInstance
}
}

很简单,先把之前的实例对象存起来,再设置成最新调用update的实例。在patch完后就恢复原来的实例。

现在来看下createComponentInstanceForVnode()方法的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建组件实例
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, // 组件的占位符vnode
parent // 组件的父实例vm
}
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options) // 调用vm._init()方法
}

这个方法主要通过new vnode.componentOptions.Ctor(options)创建一个组件实例,然后调用vm._init()方法回到我们之前创建Vue实例的逻辑,但是有两个不同的地方。一个是合并配置,它会走下面的处理:

1
2
3
4
5
6
if (options && options._isComponent) {
// 组件实例的配置合并
initInternalComponent(vm, options)
} else {
// .. 不走这
}

因为组件构造器已经在extend的时候合并了Vue的全局属性,没必要再合并一次。最后组件实例不会走下面方式的挂载:

1
2
3
4
// 使用new Vue进行挂载,组件的挂载不会走这
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}

组件的挂载定义在init钩子,如下:

1
child.$mount(hydrating ? vnode.elm : undefined, hydrating)

创建组件真实的DOM

接下来就走挂载的逻辑,这个和前面Vue实例的挂载一样。最后也是会走到vm.__patch__()方法,不同的是vm.$elundefined。所以在patch函数里面,它会执行下面条件逻辑:

1
2
3
4
5
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
}

这个逻辑就是调用createElm()方法把App组件对应的DOM虚拟节点转为真实的DOM,并且存储在vnode.elm上。方法我们上一篇分析过,这里不再累赘。这我们例子中也就是下面对应的DOM:

1
2
3
<div class="app">
{{ msg }}
</div>

patch()返回vnode.elm作为vm.__patch__()的结果存在vm.$el。这里的vm就是我们App组件实例。也就是我们我们已经得到App组件的真实DOM,并存在vm.$el上面,也就是App组件虚拟节点的vnode.componentInstance.$el

执行完App组件实例的updateComponents()函数,流程回到App虚拟节点的init()钩子。然后执行下面的代码:

1
2
3
4
5
6
7
8
9
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 把组件对应的DOM替换组件占位符
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}

通过initComponent方法会把组件对应的真实DOM赋值给vnode.elm,注意这是vnode是组件App的占位虚拟节点,然后调用insert()方法把组件插入DOM中。至此,我们App组件就渲染完成:

总结

其实,Vue组件化的渲染过程是一个深度遍历的过程。假如我们例子中的App组件中包含其他组件,也会先创建对应占位vnode。 在App实例调用update的时候创建包含组件的实例,创建组件的真实DOM并插入到App组件的DOM中,如此反复递归。最后new Vue获取最终的DOM节点插入真实DOM环境。

通过上面的分析,我们可以知道组件DOM插入是先子后父的,并且它满足树的深度优先遍历。