Vue Source Code Learning (3) Componentization Principle

Another core idea of Vue.js is component. The so-called component is to split the page into multiple components, and the CSS, JavaScript, templates, images and other resources that each component depends on are developed and maintained together. Components are resource-independent, components can be reused within the system, and components and components can be nested.

When we use Vue.js to develop actual projects, we write a bunch of components to assemble and generate pages like building blocks. In the official website of Vue.js, we also spend a lot of time introducing what a component is, how to write a component, and the properties and characteristics that a component has.

Next, we will use the Vue-cli initialization code as an example to analyze a process of Vue component initialization.

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

var app = new Vue({
el: '#app',
//h is the createElement method
render: h => h(App)
})

createComponent

数据驱动那篇博客我们在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。

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
if (typeof tag = 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 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)
}

In our chapter, we pass an App object, which is essentially a’Component ‘type, so it will go to the else logic of the above code and create a’vnode’ directly through the’createComponent ‘method. So let’s take a look at the implementation of the’createComponent’ method, which is defined in the’src/core/vdom/create-component.js’ file:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
}

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor ! 'function') {
if (process.env.NODE_ENV ! 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}

// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor = undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

data = data || {}

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)

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

// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)

// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn

if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot

// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

// install component management hooks onto the placeholder node
installComponentHooks(data)

// return a placeholder vnode
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
)

// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}

return vnode
}

It can be seen that the logic of’createComponent 'will also be a little complicated, but it is recommended to analyze the source code only to analyze the core process, and the branch process can be targeted later, so here are three key steps for the component rendering case:

Construct subclasses to construct functions, install component hook functions, and instantiate’vnode '.

Constructor subclass constructor function

1
2
3
4
5
6
const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

When we write a component, we usually create a normal object, using our App.vue as an example. The code is as follows:

1
2
3
4
5
6
7
8
import HelloWorld from './components/HelloWorld'

export default {
name: 'app',
components: {
HelloWorld
}
}

The export here is an object, so the code logic in’createComponent 'will execute to’baseCorr.extend (Ctor) ‘, where’baseCtor ‘is actually Vue. The definition of this is at the beginning of the initialization stage of Vue. The’initGlobalAPI’ function in’src/core/global-api/index.js’ has this logic:

1
2
3
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

Careful colleagues will find that the definition here is’Vue.options’, and our’createComponent ‘is’context. $options’, in fact, in’src/core/instance/init.js’ Vue prototype on the _init function has such a logic:

1
2
3
4
5
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)

This extends some of the’options’ on Vue to vm. $options, so we can also get the Vue constructor function through’vm. $options._base ‘. We will analyze the implementation of’mergeOptions’ in subsequent chapters. Now we only need to understand that its function is to merge the’options’ of the Vue constructor function and the’options’ passed by the user to the’vm. $options’.

After understanding that baseCtor points to Vue, let’s take a look at the definition of the Vue.extend function in src/core/global-api/extende.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
/**
* Class inheritance
*/
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)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super

// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}

// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}

// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)

// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}

The role of’Vue.extend ‘is to construct a subclass of’Vue’, which uses a very classic prototype inheritance method to convert a pure object into a constructor’Sub ‘inherited from’Vue’ and return, and then to the’Sub ‘The object itself extends some properties, such as extending’options’, adding global APIs, etc.; and initializing the’props’ and’computed ‘in the configuration; finally, the’Sub’ constructor is cached to avoid multiple execution of’Vue.extend ’ Repeated construction of the same subcomponent.

In this way, when we instantiate’Sub ‘, the’this._init’ logic will be executed again. It goes to the initialization logic of the’Vue 'instance, and the logic of instantiating subcomponents will be introduced in a later chapter.

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

Install component hook function

1
2
// install component management hooks onto the placeholder node
installComponentHooks(data)

We mentioned earlier that the Virtual DOM used by Vue.js refers to an open source library snabbdomOne of its features is that the hook function of various timing is exposed in the patch process of VNode, which is convenient for us to do some extra things. Vue.js also makes full use of this and implements several hook functions in the process of initializing a VNode of Component type:

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
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
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},

insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},

destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

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
}
}
}

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
}

The whole process of installComponentHooks is to merge the hook function of’components VNodeHooks’ into’data.hook ‘, and execute the relevant hook function during the VNode execution of’patch’. The specific implementation will be described in detail later in the introduction of’patch '. Attention should be paid to the merging strategy. During the merging process, if the hook at a certain time already exists in’data.hook ‘, then merge by executing the’mergeHook’ function. This logic is very simple, that is, when it is finally executed, the two hook functions can be executed in turn.

Instantiation

1
2
3
4
5
6
7
8
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

The last step is very simple, instantiate a’vnode ‘through’new VNode’ and return it. It should be noted that unlike the’vnode ‘of ordinary element nodes, the’vnode’ of the component does not have’children ‘, which is crucial, and we will mention it later in the’patch’ process.

Summary

In this section, we analyzed the implementation of’createComponent ‘and learned that it has three key logics when rendering a component: constructing a subclass to construct a function, installing a component hook function, and instantiating a’vnode’. After’createComponent ‘returns the component’vnode’, it also goes to the’vm._update ‘method, and then executes the’patch’ function. We did a simple analysis of the’patch 'function in the previous chapter, then we will do further analysis in the next section.