前面我们学习了 vue实例化过程,在其中有这么个过程 mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)
,我们今天来重点梳理下 mergeOptions
。
resolveConstructorOptions
我们先来看看 resolveConstructorOptions(vm.constructor)
,这边入参为实例的构造函数
以 new Vue
实例化为例子,此时的构造函数就是 Vue
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 这边主要就是返回Ctor.options
let options = Ctor.options
// 跳过super option changed的情况
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
}
复制代码
那么 Vue.options
又是在哪定义的呢,
在 core/global-api/index.js
中的 initGlobalAPI
能找到 options
初始化
Vue.options = Object.create(null)
// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
复制代码
可见这边主要就是初始化了 component
, directive
, filter
,当我们在函数中调用 Vue.component
, Vue.directive
, Vue.filter
为注册全局资源时就会往 Vue.options
中注入对应资源。
Vue.extend
细心的朋友在这边可能会发现 resolveConstructorOptions(vm.constructor)
,其中的 vm.constructor
并不一定是 Vue
,有时候会是 VueComponent
,那这时 Ctor.options
又是啥呢?
其实在 new Vue
之后,遇到的组件并不会直接调用 new Vue
来初始化,而是会调用 Vue.extend
来注册组件,其代码在 core/global-api/extend.js
,我们可以看看部分代码
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
// ...
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
// ...
return Sub
}
}
复制代码
可以发现 Sub
其实是个新的函数,其原型继承自 Super.prototype
,而且其静态方法 options
也是来自 Vue.options
所以回到上面 resolveConstructorOptions(vm.constructor)
,无论 vm
是 Vue
实例还是 VueComponent
实例,其实都是指向 Vue.options
mergeOptions
前面分析可知 resolveConstructorOptions(vm.constructor)
主要就是返回了 Vue.options
,我们现在进入今天的重点 mergeOptions
,其位于 core/util/options/js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 开放环境校验组件名称
// 为什么只校验child?因为parent已经校验过了
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
// 兼容写法
if (typeof child === 'function') {
child = child.options
}
// 这边有几个normalize分别对Props Inject Directives 配置进行格式化处理
// 例如Props的属性会被修改为驼峰式 Directives中的函数写法会格式化为对象
// 具体的大家可以去了解下
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 将extends mixins 的配置合并到 parent
// 注意这边的策略是先让父选项merge而不是子选项child与其合并
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)
}
}
}
// 重点部分
// 定义输出值options={}
const options = {}
let key
// 合并parent中有的选项
// 这边稍不留神就容易入坑
// 得留意这边往mergeField传入得是key而不是parent中对应得value
// 本质上是合并两者
for (key in parent) {
mergeField(key)
}
// 合并仅child中有的选项
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
函数的整体流程比较清晰
-
格式化
props
,inject
,directives
-
合并子选项
extends
,mixins
到父选项 -
调用
mergeField
合并父子选项
合并策略
前面我们分析 mergeOptions
中通过 mergeField
来合并选项,而 mergeField
也比较简单,就是根据不同的合并属性来调用不同的 strats[key]()
,并将父子属性值传入。其中的 strats
是我们分析的重点,我们称其为 策略对象
,不同的 key-value
表示对不同的属性有不同的合并策略函数
// 初始值一般为空对象{}
const strats = config.optionMergeStrategies
复制代码
在 options.js
中为其初始化了不同的 策略函数
默认策略
有子选项则返回子选项,否则返回父选项
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
复制代码
el/propsData
在开放环境抛出警告,再调用默认合并策略
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)
}
}
复制代码
lifeCycleHooks
调用 concat
合并生命周期钩子数组,同时会将子数据格式化为数组,所以在q全局中通过 Vue.mixin
的生命周期会合并到组件生命周期中,依次调用
// LIFECYCLE_HOOKS = ['beforeCreate', 'created', 'beforeMount',
// 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed',
// 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch']
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// 调用concat合并数组
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
}
// 删除重复钩子
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
复制代码
assets
返回子对象和父对象合并的值,其中子选项会覆盖父选项
// ASSET_TYPES = ['component', 'directive', 'filter']
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) {
// assertObjectType 检查是否为对象类型
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
复制代码
props/methods/inject/computed
和 assets
差不多,返回子对象和父对象合并的值,其中子选项会覆盖父选项
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
}
复制代码
watch
会先判断是否为浏览器对象原型属性,后面就是合并父子选项,其中合并策略是通过 concat
合并数组,其中会先判断父子选项是否为数组最终格式化为数组
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
}
复制代码
data
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// data需为函数类型
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
}
// 调用mergeDataOrFn
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
// 会分为有vm和无vm的情况
// 主要区别在于call中绑定的this
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// 对父子选项分别调用求值
// 返回一个新函数
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) {
// 最终值通过mergeData来合并
return mergeData(instanceData, defaultData)
} else {
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]
// 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
mergeData(toVal, fromVal)
}
}
return to
}
复制代码
data
的合并策略比较复杂一些,我们来总结一下
-
检查
data
选项是否为函数类型,否在抛出警告 -
调用
mergeDataOrFn
返回新函数,其中新函数中调用mergeData
合并父子选项 -
mergeData
中将递归遍历父数据,将其拷贝到子数据中
可以发现对 data
的合并来说,其会进行递归合并
总结
本篇文章主要梳理了在组件实例化 _init
中,对于配置选项的合并 vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
是如何进行的。其中主要分析了 mergeOptions
的实现,对于不同的属性是调用不同的策略函数进行合并的。后面将继续分析组件化的实现。