new Vue 的过程通常有 2 种场景:
- 一种是外部我们的代码主动调用
new Vue(options)
的方式实例化一个 Vue 对象; - 另一种是我们上一节分析的组件过程中*内部通过
new Sub(options)
实例化子组件。
无论哪种场景,都会执行实例的 _init(options)
方法,它首先会执行一个 merge options
的逻辑,相关的代码在 src/core/instance/init.js
中:
1 | Vue.prototype._init = function (options?: Object) { |
可以看到不同场景对于 options
的合并逻辑是不一样的,并且传入的 options
值也有非常大的不同,接下来我会分开介绍 2 种场景的 options
合并过程。
为了更直观,我们可以举个简单的示例:
1 | import Vue from "vue"; |
例子中使用了 Vue.mixin
函数,是因为 mixin 本身就是合并 options 的过程,来看 Vue.mixin 的定义:
1 | import { mergeOptions } from '../util/index' |
其实就是调用了 mergeOptions
函数,把 mixin
中的内容合并到 Vue.options
上。
外部调用场景:
当执行 new Vue
的时候,在执行 this._init(options)
的时候,就会执行如下逻辑去合并 options
:
1 | vm.$options = mergeOptions( |
这里通过调用 mergeOptions
方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor)
的返回值和 options
做合并。
1 | export function resolveConstructorOptions (Ctor: Class<Component>) { |
if 语句通过 Ctor.super
判断 Ctor
是 Vue 还是 Vue 的子类,显然在我们的例子中是 Vue, 所以 resolveConstructorOptions
函数直接返回 Vue.options
(重点)。
Vue.options
那么 Vue.options
又是在哪定义的呢,其实在 initGlobalAPI(Vue)
的时候定义了这个值,代码在 src/core/global-api/index.js
中:
1 | export function initGlobalAPI(Vue: GlobalAPI) { |
- 通过 Object.create(null) 创建一个空对象并赋值给
Vue.options
- 然后遍历
ASSET_TYPES
,这里的 ASSET_TYPES 是一个常量: 1
2
3
4
5export const ASSET_TYPES = [
'component',
'directive',
'filter'
]接着执行了
Vue.options._base = Vue
,它用于创建子类构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18export 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
}
// 核心逻辑1:创建子类构造函数
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// Weex的一些逻辑...
return vnode
}最后通过
extend(Vue.options.components, builtInComponents)
把一些内置组件扩展到Vue.options.components
上,Vue 的内置组件目前有、 和 组件,这也就是为什么我们在其它组件中使用 组件不需要注册的原因,这块儿后续我们介绍 组件的时候会详细讲。
此时的 Vue.options 大概长这样:
mergeOptions
回到 mergeOptions
这个函数,它的定义在 src/core/util/options.js
中
1 | function mergeOptions( |
mergeOptions
主要功能就是把 parent
和 child
这两个对象根据一些合并策略,合并成一个新对象并返回。有两个核心逻辑:
- 当
child
是未合并的options
时(!child._base
),递归调用mergeOptions
。将parent
分别和child.extends
、child.mixins
合并,最后的结果赋给parent
- 遍历
parent
,调用mergeField
,然后再遍历child
,如果key
不在parent
的自身属性上,则调用mergeField
mergeField
首先定义了 strat
, strat
实际上也是个函数,它的取值有两个来源,我们先看这个 defaultStrat
的定义:
1 | const defaultStrat = function(parentVal: any, childVal: any): any { |
defaultStrat
的逻辑很简单,有 childVal
就用 childVal
,没有就用 parentVal
。
再看strats
的定义:
1 | const strats = config.optionMergeStrategies; // Object.create(null) |
这里 strats
的值其实就是个空对象。strats
就是各种选项合并策略函数的集合,用来合并父 options
的 value
和子options
的 value
。
举例来说,对于生命周期函数,它的合并策略是这样的:
1 | function mergeHook ( |
这其中的 LIFECYCLE_HOOKS
的定义在 src/shared/constants.js
中:
1 | export const LIFECYCLE_HOOKS = [ |
这里定义了所有的钩子函数名称,所以对于钩子函数,他们的合并策略都是 mergeHook
函数。
下面分析一下 mergeHook 函数:
- 如果不存在
childVal
,就返回parentVal
- 否则再判断是否存在
parentVal
,如果存在就把childVal
添加到parentVal
后返回新数组;否则返回childVal
的数组。
所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
关于其它属性的合并策略的定义都可以在 src/core/util/options.js
文件中看到。
因此,在我们这个例子下,执行完如下合并后:
1 | vm.$options = mergeOptions( |
vm.$options
的值差不多是如下这样:
1 | vm.$options = { |
内部调用场景:
回忆下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js
中:
1 | export function createComponentInstanceForVnode ( |
vnode.componentOptions.Ctor
就是在createComponent
过程中通过调用Vue.extend
返回的Sub构造函数
- 所以 执行
new vnode.componentOptions.Ctor(options)
接着执行this._init(options)
- 在
Vue.extend
中定义了Sub.options
Sub.options
回顾一下这个过程,代码定义在 src/core/global-api/extend.js
中。
1 | Vue.extend = function (extendOptions: Object): Function { |
extendOptions
对应的就是的组件对象,它会和 Vue.options
合并到 Sub.opitons
中
mergeOptions
执行new vnode.componentOptions.Ctor(options)
紧接着会执行this._init(options)
,因为 options._isComponent
为 true
,那么合并 options
的过程走到了 initInternalComponent(vm, options)
逻辑
1 | Vue.prototype._init = function (options?: Object) { |
1 | export function initInternalComponent (vm: Component, options: InternalComponentOptions) { |
- 首先执行
const opts = vm.$options = Object.create(vm.constructor.options)
,这里的vm.constructor
就是子组件的构造函数Sub
,相当于:vm.$options = Object.create(Sub.options)
。 - 保存父VNode实例
parentVnode
实例到vm.$options
中 - 保存子组件的父Vue实例
parent
到vm.$options
中 - 另外还保留了
parentVnode
配置中的如 propsData、listeners 等其它的属性。
总结:这么看来,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。
最后vm.$options 的值差不多是如下这样:
1 | vm.$options = { |
总结:
,Vue 初始化阶段对于options 的合并有 2 种方式,子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。