在初始化 Vue 实例的过程中,有一段逻辑涉及到 options 的合并,这其中分为两种场景:首次初始化时 options 合并和组件 options 合并。它们合并的逻辑有所不同,这也是本文所要探究的内容。
合并配置(options)
按照惯例,沿着主线将其整理成一张逻辑图,如下:
回顾一下,在初始化 Vue 实例时,合并 options 配置逻辑如下:
// 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
)
}
从代码逻辑可以看出,合并配置分两种情况:首次初始化时配置合并和组件化配置合并。下面将一一来分析它们是如何实现合并的?
首次初始化配置合并
在首次初始化 Vue 实例时,外部传入 options 不包含属性 _isComponent ,于是执行 else 逻辑,即调用 mergeOptions 函数来实现 options 配置合并。在分析 mergeOptions 之前,先来看下 resolveConstructorOptions 是如何实现的?
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
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) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
函数接收一个参数:Ctor,数据类型为 Class<Component> ,这里传入的参数:vm.constructor ,即为 Vue 构造函数,结果返回的是 Vue.options。而 Vue.options 是在 initGlobalAPI 函数定义的,位于 src/core/global-api/index.js,相关的逻辑实现如下:
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)
首先通过 Object.create(null) 创建空对象,赋值给 Vue.options ;接着看下 ASSET_TYPES 具体指的是什么?
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
ASSET_TYPES 是一个数组,包含 component、directive、filter ,遍历该数组,分别设置为 Vue.options 属性,其值通过 Object.create 创建的空对象。除此之外,还设置属性 _base,指向构造函数 Vue 以及通过 extend 设置内置组件 KeepAlive。
回到 src/platforms/web/runtime/index.js,有这么一段逻辑:
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
分别在 Vue.options 属性 directives 和 components 设置平台指令:show 和 model 以及平台组件 Transition 和 TransitionGroup ,至此,Vue.options 具有的属性如下:
Vue.options = {
component: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
show,
model
},
filters: {},
_base: Vue
}
resolveConstructorOptions 分析完,那么接下来分析 mergeOptions 的具体实现:
/**
* 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)
// 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
}
函数接收三个参数,分别如下:
parent:数据类型为Object,在这里指的是Vue.opitons;child:数据类型为Object,在这里指的是new Vue传入的对象;vm:数据类型为Component,表示 Vue 实例。
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
开发环境下对 child 进行校验,即检查 child 是否包含属性 components,如果包含的话,则检查组件名命名是否合法,具体实现如下:
/**
* Validate component names
*/
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
export function validateComponentName (name: string) {
if (!new RegExp(`^[a-zA-Z][\-\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
这三行代码的主要作用是:如果 child 包含属性 props、inject、directives,则对它们进行规范化处理,比如名称、传入的类型等等。
// 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)
}
}
}
这段代码的主要作用是判断 child 是否包含属性 extends、mixins,如果包含的话则调用 mergeOptions 对它们进行处理。
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
这里才是对 options 进行合并的逻辑,先遍历 parent ,即 Vue.options 属性,调用函数 mergeField 进行合并;然后再遍历 child ,即外部传入的 opitons 属性,判断 parent 是否属性。如果不包含的话,则通用调用 mergeFiled 进行属性合并。那么 mergeField 又是如何实现的呢?
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
首先来看下 starts 具体指的是什么?其定义位于 src/core/unit/options.js,相关代码如下:
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
const strats = config.optionMergeStrategies
/**
* Options with restrictions
*/
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)
}
}
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)
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
/**
* Watchers.
*
* Watchers hashes should not overwrite one
* another, so we merge them as arrays.
*/
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
}
/**
* Other object hashes.
*/
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
}
strats.provide = mergeDataOrFn
从代码可以看出,starts 默认是一个空对象,然后设置各种属性,这里需要知道两个常量具体指的是什么?分别是:LIFECYCLE_HOOKS 和ASSET_TYPES ,如下:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
那么,starts 最终包含的属性如下:
starts = {
el: () => {}, // 开发环境
propsData: () => {}, // 开发环境
data: () => {},
beforeCreate: mergeHook,
created: mergeHook,
beforeMount: mergeHook,
mounted: mergeHook,
beforeUpdate: mergeHook,
updated: mergeHook,
beforeDestroy: mergeHook,
destroyed: mergeHook,
activated: mergeHook,
deactivated: mergeHook,
errorCaptured: mergeHook,
serverPrefetch: mergeHook,
component: mergeAssets,
directive: mergeAssets,
filter: mergeAssets,
watch: () => {},
props: () => {},
methods: () => {},
inject: () => {},
computed: () => {},
provide: mergeDataOrFn
}
回到 mergeField,其核心逻辑是通过传入 key,从对象 starts 获取其值,如果存在的话,则执行相对应的函数;否则执行默认的函数,即 defaultStrat,具体实现如下:
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
重新梳理下合并逻辑,根据不同的 key 采用不同的策略对其进行合并,最后返回合并后的 opiotns 。
组件化配置合并
Vue 内部在定义子组件时,设置属性 _isComponent 表示其是一个组件,那么在合并配置时则会调用函数 initInternalComponent ,具体实现如下:
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
}
}
函数接收两个参数:
vm:数据类型为Component,表示 Vue 实例;options:数据类型为InternalComponentOptions),表示传入的opitons。
先获取 Vue 构造函数 options,即 Vue.options,通过 Object.create 创建一个对象赋值 opts 和 vm.$options,也就是说,Vue.options 作为属性 opts 和 vm.$options 的原型;然后设置相关的属性,最终完成配置合并。相比首次初始化配置合并,其实现相对比较简单。
至此,合并配置的逻辑已经分析完了。