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