通过之前章节的源码分析我们知道,new Vue 的过程通常有 2 种场景
- 一种是外部代码主动调用
new Vue(options)的方式实例化一个Vue对象 - 另一种是我们上一节分析的创建组件过程中内部通过
new Sub(options)实例化子组件。
无论哪种场景,都会执行实例的 _init(options) 方法,它首先会执行一个 merge options 的逻辑,相关的代码在 src/core/instance/init.js 中:
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
}
可以看到不同场景对于 options 的合并逻辑是不一样的,并且传入的 options 值也有非常大的不同,接下来将分开介绍 2 种场景的 options 合并过程。
为了更直观,我们可以举个简单的示例:
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'Hello Vue'
}
}
}
Vue.mixin({
created() {
console.log('parent created')
}
})
let app = new Vue({
el: '#app',
render: h => h(childComp)
})
例子中使用了 Vue.mixin 函数,是因为 mixin 本身就是合并 options 的过程,来看 Vue.mixin 的定义:
// src/core/global-api/mixin.js
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
其实就是调用了 mergeOptions 函数,把 mixin 中的内容合并到 Vue.options 上。关于 mergeOptions 函数在下文介绍
1. 外部调用场景
当执行 new Vue 的时候,merge options 走的是 _init 中的 else 逻辑:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这里通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并
resolveConstructorOptions 定义在 src/core/instance/init.js
// src/core/instance/init.js
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options // 在这其实是Vue.options
if (Ctor.super) {
// ...
}
return options
}
if 语句通过 Ctor.super 判断 Ctor 是 Vue 还是 Vue 的子类,显然在我们的例子中是 Vue,所以 resolveConstructorOptions 函数直接返回 Vue.options
1.1 Vue.options
那么 Vue.options 又是在哪定义的呢,其实在 initGlobalAPI(Vue) 的时候定义了这个值,代码在 src/core/global-api/index.js 中:
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
// ...
}
- 通过
Object.create(null)创建一个空对象并赋值给Vue.options - 然后遍历
ASSET_TYPES,这里的ASSET_TYPES是一个常量:
// src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
- 接着执行了
Vue.options._base = Vue,它用于创建子类构造函数,可以看这 - 最后通过
extend(Vue.options.components, builtInComponents)把一些内置组件扩展到Vue.options.components上,Vue的内置组件目前有<keep-alive>、<transition>和<transition-group>组件,这也就是为什么我们在其它组件中使用<keep-alive>组件不需要注册的原因,这块儿后续我们介绍<keep-alive>组件的时候会详细讲。
此时的 Vue.options 大概长这样:

1.2 mergeOptions
接下来看看 mergeOptions 是怎么合并的,它定义在 src/core/util/options.js 中
// src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
// 只对未合并的 options 做处理,因为只有已合并的 options 才有 _base 属性
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
// 如果 key 不在 parent 的自身属性上
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 合并策略
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
mergeOptions 主要功能就是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。有两个核心逻辑:
- 当
child是未合并的options时,递归调用mergeOptions。将parent分别和child.extends、child.mixins合并,最后的结果赋给parent - 遍历
parent,调用mergeField,然后再遍历child,如果key不在parent的自身属性上,则调用mergeField
1.2.1 mergeField
首先定义了 strat , strat 实际上也是个函数,它的取值有两个来源,我们先看这个 defaultStrat 的定义:
// src/core/util/options.js
const defaultStrat = function(parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal;
};
defaultStrat 的逻辑很简单,有 childVal 就用 childVal ,没有就用 parentVal 。
我们再来看 strats 的定义:
// src/core/util/options.js
const strats = config.optionMergeStrategies; // Object.create(null)
这里 strats 的值其实就是个空对象。strats 就是各种选项合并策略函数的集合,用来合并父 options 的 value 和子 options 的 value。
举例来说,对于生命周期函数,它的合并策略是这样的:
// src/core/util/options.js
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res) // 作用是数组去重
: res
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:
// src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
这里定义了所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 mergeHook 函数。
下面分析一下 mergeHook 函数:
- 如果不存在
childVal,就返回parentVal - 否则再判断是否存在
parentVal,如果存在就把childVal添加到parentVal后返回新数组;否则返回childVal的数组。
所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
关于其它属性的合并策略的定义都可以在 src/core/util/options.js 文件中看到,这里不一一介绍了,感兴趣的同学可以自己看。
通过执行 mergeField 函数,把合并后的结果保存到 options 对象中,最终返回它。
因此,在我们这个例子下,执行完如下合并后:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm.$options 的值差不多是如下这样:
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
2. 内部组件场景
我们回忆一下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js 中:
// src/core/vdom/create-component.js
export function createComponentInstanceForVnode (
vnode: any,
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// ...
return new vnode.componentOptions.Ctor(options)
}
vnode.componentOptions.Ctor就是在createComponent过程中通过调用Vue.extend返回的Sub构造函数- 在
Vue.extend中定义了Sub.options
2.1 Sub.options
回顾一下这个过程,代码定义在 src/core/global-api/extend.js 中。
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
return Sub
}
extendOptions 对应的就是的组件对象,它会和 Vue.options 合并到 Sub.opitons 中
2.2 mergeOptions
实例化后接着执行 this._init(options),来看下 _init 方法:
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// ...
}
// ...
}
因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。
先来看一下它的代码实现,在 src/core/instance/init.js 中:
const opts = vm.$options = Object.create(vm.constructor.options),这里的vm.constructor就是子组件的构造函数Sub,相当于vm.$options = Object.create(Sub.options)- 保存父
Vue实例到vm.$options中 - 保存占位符
vnode到vm.$options中 - 另外还保留了占位符
vnode配置中的如propsData等其它的属性。
// src/core/instance/init.js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
这么看来,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。
因此,在我们这个例子下,执行完如下合并后,vm.$options 的值差不多是如下这样:
vm.$options = {
parent: Vue // 父Vue实例,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode // 占位符vnode,
_renderChildren: undefined,
__proto__: {
components: { },
directives: { },
filters: { },
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
}, function created() {
console.log('child created')
}
],
mounted: [
function mounted() {
console.log('child mounted')
}
],
data() {
return {
msg: 'Hello Vue'
}
},
template: '<div>{{msg}}</div>'
}
}
总结
那么至此,Vue 初始化阶段对于 options 的合并过程就介绍完了,我们需要知道对于 options 的合并有 2 种方式,子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。