合并配置
new Vue
的過程通常有2種場景,一種是外部我們的代碼主動調(diào)用new Vue(options)
的方式實例化一個Vue
對象耍共;另一種是內(nèi)部通過new Vue(options)
實例化子組件。
無論哪種場景猎塞,都會執(zhí)行實例的_init(options)
方法试读,它首先會執(zhí)行一個merge options
的邏輯,相關(guān)的代碼在src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
// 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
)
}
// ...
}
可以看到不同場景對于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)
})
外部調(diào)用場景
當(dāng)執(zhí)行new Vue
的時候倘屹,在執(zhí)行this._init(options)
的時候,就會執(zhí)行如下邏輯去合并options
:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
這里通過調(diào)用mergeOptions
方法來合并慢叨,它實際上就是把resolveConstructorOptions(vm.constructor)
的返回值和options
做合并纽匙,resolveConstructorOptions
的實現(xiàn)先不考慮,在我們這個場景下拍谐,它還是簡單返回vm.constructor.options
烛缔,相當(dāng)于Vue.options
馏段,那么這個值又是什么呢,其實在initGlobalAPI(Vue)
的時候定義了這個值践瓷,代碼在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)
})
// 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)
// ...
}
首先通過Vue.options = Object.create(null)
創(chuàng)建一個空對象毅弧,然后遍歷ASSET_TYPES
,ASSET_TYPES
的定義在src/shared/constants.js
中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
所以上面遍歷ASSET_TYPES
后的代碼相當(dāng)于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
接著執(zhí)行了Vue.options._base = Vue
当窗。
最后通過extend(Vue.options.components, builtInComponents)
把一些內(nèi)置組件擴展到Vue.options.components
上够坐,Vue
的內(nèi)置組件目前有<keep-alive>
、<transition>
和 <transition-group>
組件崖面,這也就是為什么我們在其它組件中使用<keep-alive>
組件不需要注冊的原因元咙。
那么回到mergeOptions
這個函數(shù),它的定義在src/core/util/options.js
中:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
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)
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) {
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
}
mergeOptions
主要功能就是把parent
和child
這兩個對象根據(jù)一些合并策略巫员,合并成一個新對象并返回庶香。比較核心的幾步,先遞歸把extends
和 mixins
合并到parent
上简识,然后遍歷parent
赶掖,調(diào)用mergeField
,然后再遍歷child
七扰,如果key
不在parent
的自身屬性上奢赂,則調(diào)用mergeField
。
這里有意思的是mergeField
函數(shù)颈走,它對不同的key
有著不同的合并策略膳灶。舉例來說,對于生命周期函數(shù)立由,它的合并策略是這樣的:
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
})
這其中的LIFECYCLE_HOOKS
的定義在src/shared/constants.js
中:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
這里定義了Vue.js所有的鉤子函數(shù)名稱轧钓,所以對于鉤子函數(shù),他們的合并策略都是mergeHook
函數(shù)锐膜。這個函數(shù)的實現(xiàn)也非常有意思毕箍,用了一個多層3元運算符,邏輯就是如果不存在childVal
道盏,就返回parentVal
而柑;否則再判斷是否存在parentVal
,如果存在就把childVal
添加到 parentVal
后返回新數(shù)組捞奕;否則返回 childVal
的數(shù)組牺堰。所以回到mergeOptions
函數(shù),一旦parent
和child
都定義了相同的鉤子函數(shù)颅围,那么它們會把2個鉤子函數(shù)合并成一個數(shù)組。
關(guān)于其它屬性的合并策略的定義都可以在src/core/util/options.js
文件中看到恨搓。
通過執(zhí)行mergeField
函數(shù)院促,把合并后的結(jié)果保存到options
對象中筏养,最終返回它。
因此常拓,在我們當(dāng)前這個case
下渐溶,執(zhí)行完如下合并后:
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) {
//...
}
}
組件場景
由于組件的構(gòu)造函數(shù)是通過Vue.extend
繼承自Vue
的,先回顧一下這個過程弄抬,代碼定義在src/core/global-api/extend.js
中茎辐。
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// 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)
// ...
return Sub
}
我們只保留關(guān)鍵邏輯,這里的extendOptions
對應(yīng)的就是前面定義的組件對象掂恕,它會和Vue.options
合并到Sub.opitons
中拖陆。
接下來我們再回憶一下子組件的初始化過程,代碼定義在 src/core/vdom/create-component.js
中:
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
}
// ...
return new vnode.componentOptions.Ctor(options)
}
這里的vnode.componentOptions.Ctor
就是指向 Vue.extend
的返回值Sub
懊亡, 所以執(zhí)行new vnode.componentOptions.Ctor(options)
接著執(zhí)行this._init(options)
依啰,因為options._isComponent
為true
,那么合并options
的過程走到了initInternalComponent(vm, options)
邏輯店枣。先來看一下它的代碼實現(xiàn)速警,在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
方法首先執(zhí)行const opts = vm.$options = Object.create(vm.constructor.options)
,這里的vm.constructor
就是子組件的構(gòu)造函數(shù)Sub
鸯两,相當(dāng)于vm.$options = Object.create(Sub.options)
闷旧。
接著又把實例化子組件傳入的子組件父VNode
實例parentVnode
、子組件的父Vue
實例parent
保存到vm.$options
中钧唐,另外還保留了parentVnode
配置中的如propsData
等其它的屬性鸠匀。
這么看來,initInternalComponent
只是做了簡單一層對象賦值逾柿,并不涉及到遞歸缀棍、合并策略等復(fù)雜邏輯。
因此机错,在我們當(dāng)前這個case
下爬范,執(zhí)行完如下合并后:
initInternalComponent(vm, options)
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>'
}
}
總結(jié)
那么至此,Vue初始化階段對于options
的合并過程就介紹完了弱匪,我們需要知道對于options
的合并有2種方式青瀑,子組件初始化過程通過initInternalComponent
方式要比外部初始化Vue
通過mergeOptions
的過程要快,合并完的結(jié)果保留在vm.$options
中萧诫。
縱觀一些庫斥难、框架的設(shè)計幾乎都是類似的,自身定義了一些默認(rèn)配置帘饶,同時又可以在初始化階段傳入一些定義配置哑诊,然后去merge
默認(rèn)配置,來達(dá)到定制化不同需求的目的及刻。只不過在Vue
的場景下镀裤,會對merge
的過程做一些精細(xì)化控制竞阐,雖然我們在開發(fā)自己的JSSDK的時候并沒有Vue
這么復(fù)雜,但這個設(shè)計思想是值得我們借鑒的暑劝。