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 } } } }