浅析vue混入(mixin)
vue中的混入,可以在一定程度上提高代码的复用性。通俗来说,混入类似于“继承”,当前组件对象继承于组件对象,一般情况下遵循“就近原则”。但是与继承不同的是,继承一般都跟随着属性的重写与合并,混入在不同的配置项中,有着不同的混入策略,下面会一一进行介绍vue不同配置项的混入策略。vue混入的基本流程如图所示,混入属性的合并是发生在组件的生命周期钩子调用之前的。
在我们实例化vue时,主要是调用Vue._init方法,此方法,主要功能是初始化组件状态、事件,具体代码如下:
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // 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 { // 合并vue属性 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // 初始化proxy拦截器 initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化options initLifecycle(vm) // 初始化组件事件侦听 initEvents(vm) // 初始化渲染方法 initRender(vm) callHook(vm, 'beforeCreate') // 初始化依赖注入内容,在初始化data、props之前 initInjections(vm) // resolve injections before data/props // 初始化props/data/method/watch/methods initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // 挂载元素 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
mergeOptions是合并组件的配置项,第一次实例化Vue时调用,接收两个参数,第一个参数是构造函数默认自带的属性,在项目初始化时会调用initGlobalAPI方法,会在Vue构造函数上初始化一些默认的配置,具体代码如下所示,第二个为我们实例化vue配置的属性
// 初始化全局API export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} configDef.get = () => config if (process.env.NODE_ENV !== 'production') { configDef.set = () => { warn( 'Do not replace the Vue.config object, set individual fields instead.' ) } } Object.defineProperty(Vue, 'config', configDef) // exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. Vue.util = { warn, extend, mergeOptions, defineReactive } Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API Vue.observable = <T>(obj: T): T => { observe(obj) return obj } // 初始化Vue构造函数的options,初始属性为components,directives,filters 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. // _base属性 Vue.options._base = Vue extend(Vue.options.components, builtInComponents) // 初始化vue.use api initUse(Vue) // 初始化mixins api initMixin(Vue) // 初始化extend api initExtend(Vue) // 初始化component,directive,filter initAssetRegisters(Vue) }
混入实现的主要代码如下:
1 export function mergeOptions ( 2 parent: Object, 3 child: Object, 4 vm?: Component 5 ): Object { 6 if (process.env.NODE_ENV !== 'production') { 7 // 检测组件名称是否合法 8 checkComponents(child) 9 } 10 11 if (typeof child === 'function') { 12 child = child.options 13 } 14 // 格式化属性名称 15 normalizeProps(child, vm) 16 // 格式化依赖注入内容 17 normalizeInject(child, vm) 18 // 格式化指令内容 19 normalizeDirectives(child) 20 21 // Apply extends and mixins on the child options, 22 // but only if it is a raw options object that isn't 23 // the result of another mergeOptions call. 24 // Only merged options has the _base property. 25 // 只有合并的options拥有_base属性 26 // 需要递归进行合并属性 27 // 首先合并extends和mixins 28 if (!child._base) { 29 if (child.extends) { 30 parent = mergeOptions(parent, child.extends, vm) 31 } 32 if (child.mixins) { 33 for (let i = 0, l = child.mixins.length; i < l; i++) { 34 parent = mergeOptions(parent, child.mixins[i], vm) 35 } 36 } 37 } 38 39 const options = {} 40 let key 41 for (key in parent) { 42 mergeField(key) 43 } 44 for (key in child) { 45 if (!hasOwn(parent, key)) { 46 mergeField(key) 47 } 48 } 49 // 合并属性 50 function mergeField (key) { 51 // 获取属性的合并策略 52 const strat = strats[key] || defaultStrat 53 // 调用属性合并策略,返回值为属性合并结果 54 options[key] = strat(parent[key], child[key], vm, key) 55 } 56 return options 57 }
具体某个字段的合并,调用的是mergeField方法,此方法主要是获取代码混入策略,返回值作为混入的结果。通过调试我们可以看出,混入的策略对象中包含我们常见的vue属性,如下所示:
混入的实现,采用了策略模式的设计模式,在对组件数据初始化时,会遍历组件的配置文件,根据配置文件,调用对应的策略方法。如果组件中存在不是vue指定的配置,就是策略类strats中不包含的属性,就会调用默认的合并方法defaultStrat,该方法的定义如下:
/** * Default strategy. * 默认的属性合并策略,采用就近原则,如果子级没有,就采用父级的 */ const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal }
strats是从哪来的呢? 属性合并策略strats默认会读取config中的配置文件,在项目初始化时,首先会初始化全局的api,将config文件挂载到Vue构造方法中,再者会初始化所有的属性合并策略,如下图所示:
如果我们要自定义属性合并策略,只需要覆盖掉Vue构造方法中的合并策略,也就是全局定义一个合并策略,如下所示:
// 自定义属性合并策略 Vue.config.optionMergeStrategies.methods = function (toVal, fromVal) { if (toVal && fromVal) return fromVal if (toVal) return toVal if (fromVal) return fromVal }
vue对组件配置项的每一部分执行的合并策略有所差异,每一项的合并策略,主要分为覆盖、保留,如下所示:
data的混入返回的是一个function,参数保持对父级的引用,在初始化state的时候会调用,data的混入策略如下:
// 定义data的属性合并策略 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) }
/** * Data * 合并对象 or 数组 * 利用闭包,返回一个包含私有参数的执行方法 */ export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { // in a Vue.extend merge, both should be functions // 在Vue.extend场景中??? 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) { return mergeData(instanceData, defaultData) } else { return defaultData } } } }