文章首发于个人博客 小灰灰的空间
选项合并策略分析
在对 props
inject
directive
三个选项转换成统一格式之后,即开始合并选项,看一下选项合并的代码
// core/instance/init.js
// 开始合并选项
const options = {}
let key
for (key in parent) {
// 首先将父实例中的选项合并到目标对象中
mergeField(key)
}
for (key in child) {
// 遍历 child,如果 child 中的属性不存在 parent 自身上,则将属性合入 parent
if (!hasOwn(parent, key)) {
//
mergeField(key)
}
}
function mergeField (key) {
// starts 对象上定义了各种选项的合策略
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
合并选项时,优先使用自定义合并策略,如果自定义选项策略不存在,则使用默认合并策略。 starts
对象中的每个 key 都对应了一种选项的合并策略
默认合并策略
默认的选项合并策略,如果子类选项存在则使用子类选项覆盖父类的选项
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
el 选项合并策略
el 选项提供了一个在页面上已存在的 DOM
元素作为 Vue
实例的挂载目标。改选项只在 Vue
实例上才存在,其他的子类构造器上不允许存在 el
选项
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
// 只有 Vue 实例才拥有 el 选项,其他子类构造器不允许存在 el 选项
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
data 选项合并策略
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') { // 确保子类的 data 类型必须是一个函数而不是对象,使用一个对象返回一个 data 类型可以实现服复用,组件实例之间的数据不会相互影响
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
}
// 调用 cmergeDataOrFn 合并选项
return cmergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
// 传入的选项为组件中的选项,因此不存在 vm 实例·
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
// data 选项在父类和子类同时存在时返回一个函数,在后面响应式系统进行初始化时调用方法真正的合并选项
return mergeData(
// 分别将子类实例和父类实例的 data 函数执行后的返回结果传递给 mergerData 进行合并
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// Vue 实例 中挂在的 data 属性,这里可以是一个对象
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) {
// 将实例的选项,与 Vue 构造器中的选项进行合并
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
分析上面的代码可以发现,在 Vue
实例初始化过程中, data
选项并没有真正的合并,只是返回了一个函数,返回的函数内部返回了 mergeData
的执行结果,也就是说, data
选项的正式合并
是在 mergeData
函数中。来看看 mergeData
函数的实现
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
// 通过 Reflect.ownKeys 可以获取到 Symbol 属性
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
// 父类选项在子类中不存在,将父类选项添加到子类中并响应式化
set(to, key, fromVal)
} else if (
// 父类选项在子类中已经存在并且不相等且都是普通对象,进行递归
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
通过分析 mergeData
方法发现, data
选项的合并原则就是,将父类的 选项合并到子类上,如果父类和子类的选项存在冲突(例如:对象属性都存在,数据类型不一致或者值不同),则保留子类的选项。
如果选项存在嵌套的情况,则需要递归调用进行合并。
知识点
ES6 中引入的 Symbol
类型在作为对象属性时时不可枚举中,在使用 Object.getOwnPropertyNames()
也不会返回 Symbol
对象的属性,但是可以使用 Object.getOwnPropertySymbols()
来获取 Symbol
属性
在上面的 mergeData
方法中, 使用 Reflect.ownKeys
方法可以获取包括 Symbol
对象属性之内的所有属性,它的返回值等同于 它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
。
最后在开发中, 创建 Vue
实例时,提供选项中 data
可以是一个对象,而在创建组件时, data
选项须为一个函数。可以这样理解,创建组件的目的是为了实现服复用, data
作为一个函数时,在
创建多个组件实例是,组件实例之间的数据不会相互应用,因为每个组件实例的 data
数据都是组件模板中 data
的副本。
Vue 构造器内置选项合并
Vue 构造器中内置了 components
、directive
、filter
几个选项,无论是 Vue 根实例,还是组件实例都需要和这些选项进行合并。
// core/util/options.js
// Vue 默认选项的合并,这些选项会合并到每一个 Vue 实例中
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null) // 创建一个️对象,使其原型指向父类的资源选项,对于内置的 组件、指令、过滤器需要通过原型的方式来进行调用
if (childVal) {
// 开发环境下校验选项的合法性, component directive filter 这些选项需要是一个对象
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
这些选项的合并策略也比较简单,先创建一个空对象,该控对象的原型指向父类的资源选项,然后将子类的选项赋值给整个空对象