Vue 配置合并
# 参数合并的场景
# new Vue
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
2
3
4
5
# Vue.extend
这种情况下,mergeOptions 的参数有两个:
Super.options
即Vue.options
extendOptions
即传递给Vue.extend
的参数
Vue.extend = function (extendOptions) {
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.options = mergeOptions(Super.options, extendOptions)
Sub['super'] = Super
// ...
Sub.extend = Super.extend
// ...
// 缓存父类构造器的配置
Sub.superOptions = Super.options
// 缓存传入的选项对象
Sub.extendOptions = extendOptions
// 缓存自身的配置对象
Sub.sealedOptions = extend({}, Sub.options)
return Sub
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
暂时只考虑这两种简单的场景,因为 extend
方法的调用不一定非得是 Vue 构造器,还可以是通过 Vue.extend
方法生成的子类构造器
let Base = Vue.extend({
// ...
})
let Child = Base.extend({
// ...
})
2
3
4
5
6
7
这里还是以 创建 vue 实例,即 new Vue()
的这种情况为例
# mergeOptions
选项合并的重点是将
用户自身传递的 options 选项
和Vue 构造函数自身的选项配置
合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
2
3
4
5
该方法接收三个参数,首先第一个参数是通过 resolveConstructorOptions
这个方法获取的
# resolveConstructorOptions
该方法是用来从实例的构造器上去获取配置对象
的
function resolveConstructorOptions(Ctor) {
var options = Ctor.options
// extend 场景
if (Ctor.super) {
// 递归查询
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
// 如果发生了变化,则需要更新到最新的配置
if (superOptions !== cachedSuperOptions) {
Ctor.superOptions = superOptions
// 获取发生变化的配置项,这里会将 Ctor.options 与 Ctor.sealedOptions 进行对比
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
// 只把发生修改的属性复制到extendOptions上
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
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
上面将 vm.constructor
也就是 Vue
传入,在 resolveConstructorOptions
方法内部,
- 判断如果
Ctor
上有super
属性的话,也就是Vue.extend
的场景下,会进一步对options
进行处理 - 没有的话就直接返回构造器上的 options 属性了。
当前 Vue 构造器上没有 super
,所以就直接返回了 Vue.options
。
那 Vue.options
这个属性又是什么时候定义的呢?
# Vue.options
其实在 实例化 Vue 之前,在源码中已经对 Vue 做了一系列的初始化操作,比如
# 给 Vue 的原型对象上混入属性和方法
function Vue(options) {
this._init(options)
}
// 主要是在Vue的原型链上挂载一些方法
initMixin(Vue) // Vue.prototype._init
stateMixin(Vue) // Vue.prototype.$set/$delete/$watch
eventsMixin(Vue) // Vue.prototype.$on/$once/$off/$emit
lifecycleMixin(Vue) // Vue.prototype._update/$forceUpdate/$destroy
renderMixin(Vue) // Vue.prototype._render/$nextTick/renderHelpers
2
3
4
5
6
7
8
9
10
# 给 Vue 构造器初始化静态属性和静态方法
export function initGlobalAPI(Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive,
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = (obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
// Vue.options.components = {}
// Vue.options.directives = {}
// Vue.options.filters = {}
;['component', 'directive', 'filter'].forEach((type) => {
Vue.options[type + 's'] = Object.create(null)
})
// 这个 _base 属性也很重要
Vue.options._base = Vue
// 内置组件 `KeepAlive`
extend(Vue.options.components, builtInComponents)
// Vue.use()
initUse(Vue)
// Vue.mixin()
initMixin(Vue)
// Vue.extend()
initExtend(Vue)
// Vue.component()
// Vue.directive()
// Vue.filter()
initAssetRegisters(Vue)
}
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
可以看到 Vue.options 属性就是在 initGlobalAPI
这个方法中进行初始化的。
除此之外,针对不同的平台,还会在 options 属性上添加特定于平台的几个内部指令以及组件的定义,以 web 平台为例:
- 指令包括:
model
,show
- 组件包括:
Transition
,TransitionGroup
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
2
通过 debug
,此时 Vue.options
大概长这个样子
_base
属性,指向的是 Vue 自身components
属性中已经包含了三个内置组件:KeepAlive、Transition、TransitionGroup
directives
属性中也包含了两个内置指令:model、show
这样 mergeOptions
的第一个参数 Vue.options
就已经找到了,然后看下第二个参数:
以下面这个例子为例,
import App from './app'
new Vue({
el: '#app',
data() {
return {
name: 'Vue',
}
},
render(h) {
h(App)
},
})
2
3
4
5
6
7
8
9
10
11
12
mergeOptions 的第二个参数 child 就是 new Vue()
时传入的选项对象
。
例子中只传入了 el、data、render 三个选项,所以不涉及 normalize 相关的规范化处理,然后就是 mergeField 的过程
export function mergeOptions(parent, child, vm?: Component): Object {
// 组件校验
checkComponents(child)
if (typeof child === 'function') {
child = child.options
}
// 规范化 props 配置
normalizeProps(child, vm)
// 规范化 inject 配置
normalizeInject(child, vm)
// 规范化 directives 配置
normalizeDirectives(child)
// 针对extends扩展的子类构造器
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)
}
// 再遍历子对象上的key,只处理父对象上不存在的属性
for (key in child) {
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
}
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
选项合并过程中更多的不可控在于不知道用户传递了哪些配置选项,这些配置是否符合规范,是否达到合并配置的要求。因此每个选项的书写规则需要严格限定,原则上不允许用户脱离规则外来传递选项。因此在合并选项之前,很大的一部分工作是对选项的校验。
校验 & 规范化处理后,剩下的流程就是具体的合并操作了
- 首先是遍历了 Vue.options 上的所有属性
- 然后遍历了 vue 实例的 options 配置
- 两种情况下都进行了 mergeField 操作
# 合并策略
Vue 允许用户自定义选项的合并策略 (opens new window)。
另外如果某个属性 key 没有配置合并策略,就会走默认的策略。配置合并策略其实都是针对 选项对象
的,比如:props、data、lifecyle hooks、components、methods、watch 等等。
# 默认策略
默认的合并规则很简单,就是: 子对象上如果定义了就用子的,否则就用父亲的
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal
}
2
3
# data 的合并
- data 的合并策略,最终会返回一个函数
mergedInstanceDataFn
- 在
mergeData
的过程中,如果属性是响应式的,会被跳过 - 如果不是响应式的对象,且 parent 上存在但 child 上不存在,则将它变成响应式对象之后再添加到 child 上
- 如果父子对象上都存在且不相等,
strats.data = function (parentVal, childVal, vm?: Component): ?Function {
// ...省略
return mergeDataOrFn(parentVal, childVal, vm)
}
function mergeDataOrFn(parentVal, childVal, vm?: Component): ?Function {
// ...省略
// 这里返回的是一个函数,真正求值是在 initState 的时候
return function mergedInstanceDataFn() {
// 如果 data 是一个 function 就以 vm 为 this 调用
const instanceData =
typeof childVal === 'function' ? childVal.call(vm, vm) : childVal
const defaultData =
typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal
// child & parent 都存在就合并
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
// child 不存在,则使用 parent 的
return defaultData
}
}
}
function mergeData(to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// 如果对象是响应式的就跳过
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
// 如果 child 上不存在,且不是响应式的对象,则将它变成响应式对象之后再添加到child上
if (!hasOwn(to, key)) {
Vue.set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 递归
mergeData(toVal, fromVal)
}
}
return to
}
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
# provide 的合并
provide 的合并和 data 的合并差不多,跳过。
strats.provide = mergeDataOrFn
# lifecyle hooks 的合并
生命周期钩子函数的合并流程如下:
1.如果子和父都拥有钩子定义,则将子的和父的钩子合并, 合并的结果是将子的追加到父的后面
2.如果父不存在钩子,子存在时,则以数组形式返回子的钩子选项
3.当子不存在钩子选项时,则以父的返回
4.对结果进行去重处理
function mergeHook(
parentVal: ?Array<Function>,
childVal: ?(Function | ?Array<Function>)
): ?Array<Function> {
// prettier-ignore
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
举个例子
var Parent = Vue.extend({
mounted() {
console.log('parent')
},
})
var Child = Parent.extend({
mounted() {
console.log('child')
},
})
var vm = new Child().$mount('#app')
// 输出结果:
parent
child
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 资源类选项合并
包含components
,directives
以及filters
,这几个选项必须是对象,他们的合并策略比较简单:
- 首先创建了一个以父的资源选项为原型的对象
- 然后将子的定义拷贝到这个对象,如果父上存在同名的属性,就会被覆盖掉
- 最后返回这个对象
function mergeAssets(
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
// 创建一个原型指向父的资源选项的对象
const res = Object.create(parentVal || null)
if (childVal) {
// 将子选项的值赋值给这个对象
return extend(res, childVal)
} else {
return res
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
举个例子
const KeepAlive = {
name: 'KeepAlive',
abstract: false,
}
let vue = new Vue({
components: { KeepAlive },
})
console.log(vue.$options.components)
2
3
4
5
6
7
8
9
10
返回值形式如下
{
KeepAlive: {
name: "KeepAlive",
abstract: false,
},
[[Prototype]]: {
KeepAlive: {
name: 'keep-alive',
abstract: true,
// ...
},
Transition: {
// ...
},
TransitionGroup: {
// ...
},
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# watch 的合并
- 子不存在,返回以父为原型的对象
- 父不存在,返回子
- 都存在时,则遍历子上的所有定义
- 父上存在,则将子的定义追加到父的数组中返回
- 父上不存在,则将子的定义以数组的形式返回
strats.watch = function (parentVal, childVal, vm, key) {
// const nativeWatch = ({}).watch
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
// 子不存在,直接返回以父为原型的对象
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
// 父不存在,则返回子
if (!parentVal) return childVal
const ret = {}
// 从父上拷贝一份到 ret
extend(ret, parentVal)
// 遍历子
for (const key in childVal) {
// 从父上获取当前key的值
let parent = ret[key]
// 从子上获取当前key的值
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
// prettier-ignore
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
30
31
32
33
34
35
36
举个例子
let Parent = Vue.extend({
data() {
return {
msg: 'Parent',
}
},
watch: {
msg() {
console.log('Parent change')
},
},
})
let Child = Vue.extend({
data() {
return {
msg: 'Child',
}
},
watch: {
msg() {
console.log('Child change')
},
},
})
let vm = new Child()
vm.msg = 'hello'
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
执行结果
parent change
child change
2
# 其他选项的合并
包括 props
、methods
、inject
、computed
选项的合并,规则也很简单
- 父不存在则直接返回子
- 都存在,则用子去覆盖父
// prettier-ignore
strats.props =
strats.methods =
strats.inject =
strats.computed =
function (parentVal, childVal, vm, key): ?Object {
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
# 合并结果
合并完成之后,vm.$options 大概长下面这个样子
vm.$options = {
_base: function Vue() {
// ...
},
components: {},
data: function mergedInstanceDataFn() {
// ...
},
directives: {},
el: '#app',
filters: {},
created: [
function created() {
console.log('this is main.js')
},
],
render: function (h) {},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18