合并配置
我们创建组件实例时,都会调用vm._init
去初始化组件。而初始化组件的首要任务就是将跟组件实例有关的所有配置合并并返回新的配置对象,比如通过Vue.extend
继承而来的配置、通过Vue.mixins
或组件选项对象里mixin
选项混合而来的配置等等。
根组件 VS 子组件
我们知道,创建组件实例时一般有两种情况:用户通过new Vue(options)
显示创建、渲染组件时创建子组件,而这两种情况合并配置的方式也是不一样的。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
// merge options
if (options && options._isComponent) {
// 子组件合并配置
initInternalComponent(vm, options)
} else {
// 根组件合并配置
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
根组件
对于根组件来说,会将(经过处理的)vm.constructor.options
和调用new Vue(options)
传入的options
合并。
这里我们需要弄明白:第一,vm.constructor.options
是怎么来的以及有哪些内容;第二,resolveConstructorOptions
是做什么用的。
Vue.options
我们先忽略继承的情况,先讨论根组件实例的vm.constructor
为构造函数Vue
的情况。在initGlobalAPI(Vue)
初始化全局 API 的时候,以及在导出Vue
的时候,已经对Vue.options
进行了初始化和处理。
// src/core/global-api/index.js
import builtInComponents from '../components/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
// 初始化 components、directives、filters
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 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
// 添加内置组件定义
extend(Vue.options.components, builtInComponents)
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/platforms/web/runtime/index.js
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
export default Vue
2
3
4
5
6
7
// src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
2
3
4
5
6
// src/core/components/index.js
import KeepAlive from './keep-alive'
export default {
KeepAlive
}
2
3
4
5
6
最终,Vue.options
的数据结构大概是如下这样。
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
2
3
4
5
6
7
8
9
10
11
12
13
resolveConstructorOptions
更新:全局方法Vue.component/directive/filter
也会导致Vue.options
增加新的内容。
若是想详细了解本节的内容,请先了解Vue.extend
的实现。
resolveConstructorOptions
的主要作用是,若是vm.constructor
是继承而来的构造函数,需要重新对vm.constructor.options
进行重新合并,以加入调用vm.constructor.mixin()
对vm.constructor.options
的修改以及调用vm.constructor.super.mixin
对vm.constructor.options.super
的修改。
主要注意的是,为了保证合并的顺序,vm.constructor.super.options
需要重新计算,vm.constructor.extendOptions
也要重新更新,最后再mergeOptions(vm.constructor.super.options, vm.constructor.extendOptions)
来得到vm.constructor.options
。
/**
* 返回最新的 Ctor.options,以及更新 Ctor.extendOptions
*
* 1. 若 Ctor 不是通过 Vue.extend 继承而来的,直接返回 Ctor.options
* 2. 否则,返回计算而来的最新的 Ctor.options。此处要考虑的问题是
* a. 继承的基类 Ctor.super.options 可能发生变化(通过调用 Ctor.super.mixin() 而造成的)
* b. Ctor.options 可能发生变化(通过调用 Ctor.mixin() 而造成的)
*/
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
// 如果存在父类,即该 Ctor 是继承而来的子类
if (Ctor.super) {
// 当前计算得的最新的 Ctor.super.options
const superOptions = resolveConstructorOptions(Ctor.super)
// 子类继承时保存的 Ctor.super.options
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
// 动态更新 Ctor.extendOptions,以确保其包含了通过 Ctor.mixin 添加、修改的选项(配置合并不会出现删除的情况)
extend(Ctor.extendOptions, modifiedOptions)
}
// 基于最新的 Ctor.super.options 和 Ctor.extendOptions 合并配置
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
/**
* 返回通过调用 Ctor.mixin 从而导致 Ctor.options 里选项改变的那些选项及其值
*/
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
let modified
// 最新的 Ctor.options(可能已经通过 Ctor.mixin 改变了)
const latest = Ctor.options
// 上一次继承 Super 时调用 Super.extend(extendOptions) 传入的选项对象
const extended = Ctor.extendOptions
// 上一次继承 Super 时(合并)Ctor.options 后的副本
const sealed = Ctor.sealedOptions
for (const key in latest) {
// Ctor.mixin 是通过 mergeOptions 合并选项的,返回的 value 都是新的引用对象
if (latest[key] !== sealed[key]) {
if (!modified) modified = {}
// 返回 Ctor.options 里改变的选项值(经过去重)
modified[key] = dedupe(latest[key], extended[key], sealed[key])
}
}
return modified
}
/**
* 数据去重,若选项值不是数据,直接返回 latest,否则返回那些在 latest 里 &&(在 extended 里 || 不在 sealed 里的)
*/
function dedupe (latest, extended, sealed) {
// compare latest and sealed to ensure lifecycle hooks won't be duplicated
// between merges
if (Array.isArray(latest)) {
const res = []
sealed = Array.isArray(sealed) ? sealed : [sealed]
extended = Array.isArray(extended) ? extended : [extended]
for (let i = 0; i < latest.length; i++) {
// push original options and not sealed options to exclude duplicated options
// 筛选出:曾经在 extended 里以及 后来通过 Ctor.mixin 加入的(即不在 sealed 里)
if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
res.push(latest[i])
}
}
return res
} else {
return latest
}
}
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
mergeOptions
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
// 检查 option 里的 components 的 name 是否符合要求
checkComponents(child)
}
if (typeof child === 'function') {
// child 是构造函数
child = child.options
}
// 标准化 props、inject、directives 为对象格式
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, 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) {
// 先合并 parent 里有的选项
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
// 再合并 child 里有但 parent 里没有的选项,避免重复合并
mergeField(key)
}
}
function mergeField (key) {
// 优先使用选项单独的合并策略,没有的话使用默认策略
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
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
mergeOptions
主要的功能就是,将parent
和child
配置进行合并,包括要合并child
配置的extends
和mixins
。在合并具体选项时,我们可以看到不同的选项合并策略函数strat
可能是不一样的,如果不存在已知的合并策略函数,则将使用默认的合并策略函数。需要注意的是,开发者还可以提供自定义的合并策略函数。具体的合并策略,我们将在不同选项的合并策略
详细描述。
子组件
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,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
子组件在调用的vm._init
时传入的options._isComponent
为true
,因此是通过initInternalComponent(vm, options)
来合并配置的。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// 子组件的占位 VNode
const parentVnode = options._parentVnode
// 创建子组件时的活动实例
opts.parent = options.parent
opts._parentVnode = parentVnode
// 将组件占位 VNode 上有关组件的数据,转存到 vm.$options 上
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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
initInternalComponent
主要做的是,基于vm.constructor.options
创建vm.$options
,并将如下数据转存到vm.$options
,方便后续使用。
- 创建组件占位 VNode 时存储在
vnode.componentOptions
里的数据比如propsData
、listeners
、children
、tag
等 - 创建子组件时的选项
options
上的parent
、render
、staticRenderFns
等数据
export function createComponent (
// ...
) {
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
// vnode.componentOptions
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
不同选项的合并策略
组件选项对象里存在着各种选项,而各个选项的合并策略可能是不同的,需要在合并时通过选项的key
获取到对应的选项策略函数,这些选项策略函数都预置在config.optionMergeStrategies
对象里,而config.optionMergeStrategies
对象初始时是空对象,后续会针对不同选项添加对应的选项策略函数。
// src/core/util/options.js
import config from '../config'
// ...
const strats = config.optionMergeStrategies
export function mergeOptions (
// ...
): Object {
// ...
function mergeField (key) {
// 优先使用选项单独的合并策略,没有的话使用默认策略
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/core/config.js
export default ({
/**
* Option merge strategies (used in core/util/options)
*/
optionMergeStrategies: Object.create(null)
}
2
3
4
5
6
7
根据选项策略的不同,我们将这些选项分为以下几类:
data
、provide
选项- 生命周期钩子函数
- 资源选项(
component
、directive
、filter
) watch
选项props
、methods
、inject
、computed
选项- 默认合并策略
- (开发模式)
el
、propsData
选项
BTW,以下涉及的代码主要在src/core/util/options.js
文件里。
data、provide 选项
按以下步骤来合并data
数据(以下的child
、parent
代表的是子/父配置的data
选项的值):
- 将
child
、parent
数据若是函数,则执行函数获得返回的对象 - 若
parent
不存在,返回child
- 若
child
不存在,返回返回parent
- 遍历
parent
的属性key
- 若
child[key]
不存在,则child[key]
取用parent[key]
的值 - 若
child[key]
存在,且child[key]
和parent[key]
都是对象,则递归合并child[key]
和parent[key]
- 若
这里需要注意的是,合并配置时vm
不存在(即是调用Vue.extend
、Vue.mixin
合并配置生成构造函数的options
时)和vm
存在(生成组件实例的$options
时)的情况略微有些不同。
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal)
}
}
return to
}
/**
* Data
*/
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// vm 不存在,即调用`Vue.extend`、`Vue.mixin`合并配置生成构造函数的`options`时
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// 返回一个 data function,等真正实例化的时候再调用
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// vm 存在,即生成组件实例的`$options`时
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
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
provide
选项类似data
选项。
strats.provide = mergeDataOrFn
生命周期钩子函数
同一生命周期钩子合并成一数组,并且parent
的钩子排在数组前面,会优先执行。
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
2
3
4
5
6
7
8
9
10
11
12
13
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
资源选项(component、directive、filter)
child
里options
的资源的每一项覆盖parent
里options
的资源的每一项。
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
2
3
4
5
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
watch
child
的watch
选项不存在,则采用parent
的watch
选项parent
的watch
选项不存在,则采用child
的watch
选项不存在- 若都存在,将同名的 Watcher 合并成数组,且
parent
的 Watcher 排在前面优先执行(单个的 Watcher 也以数组形式返回)
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
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
props、methods、inject、computed 选项
存在同名的key
,则child
将覆盖parent
。
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
默认合并策略
如果 childVal 存在则使用 childVal,否则使用 parentVal
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
2
3
4
5
(开发模式)el、propsData 选项
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
2
3
4
5
6
7
8
9
10
11
将 config 挂载在 Vue 上
经过以上的分析,各种选项的策略函数都存在在config.optionMergeStrategies
对象上,且最终config
对象会挂载在Vue
上,以供开发者使用。
// src/core/global-api/index.js
import config from '../config'
// ...
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
因此,开发者可通过Vue.config.optionMergeStrategies
来获取或自定义添加选项的合并策略函数了,比如const mountedMergeFn = Vue.config.optionMergeStrategies.mounted
来获取mounted
钩子函数的合并策略函数。