配置合并
vue
是一个学习成本很低的前端框架,开发者实际上只需要关注new Vue(options)
中的配置即可,页面渲染,响应式数据的实现,vue
在背后做了大量的工作,本文主要分析vue
实例以及组件实例的options
配置是如何合并的。
vue
中定义了自己的默认配置,然后将用户的配置和默认配置进行合并,这种代码设计方式在我们日常开发中野会用到,比如封装基础的ajax
请求,默认请求方式是POST
,但用户可以自定义请求方式为GET
,只是vue
在合并的实现上在代码的组织上更加精细。
Vue.options
Vue
构造函数的静态属性options
是在src/core/global-api/index.js
中定义的.其中的ASSET_TYPES
常量定义在src/shared/constants.js
中
export function initGlobalAPI (Vue: GlobalAPI) {
...
Vue.options = Object.create(null) //创建空对象
ASSET_TYPES.forEach(type => { //遍历添加属性
Vue.options[type + 's'] = Object.create(null)
})
initMixin(Vue)
...
}
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
从上面的代码可以看到在执行initGlobalAPI
方法后,Vue.options
上会有components,directive,filters
三个属性
Vue.options = {
components: {},
directives: {},
filter: {}
}
initGlobalAPI
在src/core/index.js
文件中执行。
Vue.mixin
在initGlobalAPI
中还执行了了initMixin
方法,在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
}
}
可以看到initMixin
方法给构造函数上添加了mixin
方法,mixin
方法中调用mergeOptions
方法对传入的mixin
和Vue.options
进行合并,然后返回Vue
看了
mixin
方法,我们可以看到该方法可以链式调用Vue.mixin(o1).mixin(o2)
,这种设计方式在库和框架中很常见,比如jQuery
的很多方法都会返回jQuery
构造函数,以能实现链式调用.
mergeOptions
方法
mergeOptions
方法是用来对非组件的options
进行合并以及在vue.mixin
方法中进行调用,在src/core/util/options.js
中定义了该方法.下面我们来分析下该方法
在src/core/config.js
中定义了合并策略
export default ({
optionMergeStrategies: Object.create(null),
...
})
可以看到optionMergeStrategies
最初是一个空对象,在src/core/util/options.js
中引入,并给该对象添加各种策略(方法)。
//src/core/util/options.js 伪代码
const strats = config.optionMergeStrategies
strats = {
(el=propsData) : fn
data:fn,
[hook]: fn,
watch: fn,
(props = methods = inject = computed): fn,
provide: mergeDataOrFn
}
这里使用了设计模式中的策略模式来设计配置项合并,关于策略模式请参考js设计模式之策略模式
下面我们来看看mergeOptions
方法
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
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) {
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
}
可以看到对于
options
中不同的属性,合并策略是不同的。
钩子函数的合并
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
/**
* Hooks and props are merged as arrays.
*/
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
}
示例
示例代码
Vue.mixin
// index.js
import Vue from 'vue'
import AppForMergeOptionsTest from '@components/AppForMergeOptionsTest.vue'
Vue
.mixin({
created() {
console.log('parent created')
}
})
.mixin({
testMixIn: 'testMixin'
})
new Vue({
el: '#app',
render(h) {
return h(AppForMergeOptionsTest)
}
})
// AppForMergeOptionsTest.vue
<template>
<div>
{{msg}}
</div>
</template>
<script>
export default {
name:"AppForMergeOptionsTest",
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'hello world'
}
}
}
</script>
执行Vue.mixin
方法,进入
Vue.mixin = function (mixin) { /mixin{created: fn}
this.options = mergeOptions(this.options, mixin);
return this;
};
执行mergeOptions
方法,针对本文中第一个mixin({created: fn})
,最终在mergeHook
里返回的res
为created:[f]
,第一个合并后的Vue.options
如下图所示
在执行第二个mixin
时,由于没有指定testMixIn
具体的策略,会执行默认策略
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
可以看到执行完两次mixin
,Vue.options
是什么?
分组件和非组件实例进行配置合并
在src/core/instance/init.js
中的_init
方法中进行配置合并时分为两种类型
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
- 组件实例调用
initInternalComponent
方法进行合并配置 - 非组件实例调用
mergeOptions
方法进行配置合并
initInternalComponent
export function initInternalComponent (
vm: Component, options: InternalComponentOptions) {
...
const opts = vm.$options = Object.create(vm.constructor.options)
...
}
组件实例的vm.$options
原型为vm.constructor.options
,这里的vm.constructor
指向子组件实例的构造函数Super
。
我们回顾下Super
构造函数创建方法
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
}
}
这里的extendOptions
就是组件对象
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub.options
是将Vue.options
和组件option
进行合并,因此从const opts = vm.$options = Object.create(vm.constructor.options)
这行代码可以看到,组件实例vm.$options
中并没有组件option
中定义的属性,而是在vm.$options.__proto__
上。