前面我们学习了 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
的实现,对于不同的属性是调用不同的策略函数进行合并的。后面将继续分析组件化的实现。