源码分析 Vue.mixin(混入语法)原理 mergeOptions(合并配置)
Mixin
Vue提供了一个混入语法 mixin,使复用代码变得更加灵活, 在Vue中可以使用两种方法来混入对象,包括组件内部混入:
const myMixin = {
created() {
console.log('local mixin')
}
}
// 组件内部
export default {
name: '',
mixins: [myMixin],
created() {
console.log('component')
}
}
以及全局混入:
Vue.mixin({
created() {
console.log('vue mixin')
}
})
现在这个地方就有两个个问题了。
1.全局混入后为什么在所有的组件中都能访问到混入的这个属性?
2.当组件中也存在与混入对象上相同的属性时,该怎么处理呢?
带着这两个问题,我们去看源码吧!
全局混入 Vue.mixin
Vue.mixin的源码在src/core/global-api/mixin.js中:
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
// this 指向Vue
this.options = mergeOptions(this.options, mixin)
return this
}
}
其实这段代码也就是给Vue上的options属性与混入的对象属性进行了mergeOptions操作,并把返回的值重新赋值给了Vue.options。继续看mergeOptions的逻辑,定义在src/core/util/options.js中:
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*/
// 起始时这是一个空对象
const strats = config.optionMergeStrategies
/**
* Merge two option objects into a new one.
* 将两个选项配置合并成一个新的
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
// 如果合并的对象中如果有components属性,需要检查属性上key的命名是否规范
checkComponents(child)
}
// 第二个参数如果是函数,则取函数上面的options
if (typeof child === 'function') {
child = child.options
}
// 规范化props属性
normalizeProps(child, vm)
// 规范化inject属性
normalizeInject(child, vm)
// 规范化directives属性
normalizeDirectives(child)
// 若child对象上存在extends,将扩展属性也合并到Vue.options上面
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
// 若child上面存在mixins属性,依次遍历mixins数组,合并到Vue.options上面
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
// 初始化空对象
const options = {}
let key
// 遍历Vue.options的key,调用mergeField
for (key in parent) {
mergeField(key)
}
// 遍历child对象上的key,如果不是Vue.options上的属性,调用mergeField
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 针对传入的属性key不同,定义有不同的函数,从而执行不同的合并策略函数
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
可以看到这个函数是将传入的对象的一些属性做了一些规范化,并针对传入对象上的不同的key执行不同的strat[key]函数。vue内部针对一些属性定义了strat[key]函数,包括data、生命周期函数、静态方法(conponents、directives、filter)、watch、props、methods、inject、computed等。接下来我们分析下生命周期函数以及部分属性的合并函数:
1.生命周期函数合并
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
// 分为三种场景,见下文
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
生命周期的合并函数都是mergeHook,分为三种场景:
- 传入对象没有对应属性,直接返回Vue.options上面的生命周期函数;
- 传入对象有属性,Vue.options上面没有属性,将传入对象的函数转换成数组的形式存放;
- 传入对象与Vue.options上都存在相同的生命周期函数,则使用concat拼接成新数组,传入对象的生命周期函数放在数组后面。
2.props、methods等属性合并
/**
* Other object hashes.
*/
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)
}
// 以props为例,父没有props属性,返回child
if (!parentVal) return childVal
const ret = Object.create(null)
// 父上面的值复制到空对象ret上面
extend(ret, parentVal)
// 以props为例,将child上面的属性赋值到ret上面,有相同值则直接覆盖
if (childVal) extend(ret, childVal)
return ret
}
props、methods、inject、computed的合并策略方法是相同的,较为简单。
子对象的props与父对象上的props等有key相同的属性值的时候,子对象上面的值直接覆盖父对象上面的值。
3.默认方法
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
对与vue内部未定义合并方法的属性,则执行默认合并方法,子对象有值,直接返回子对象的值,否则返回父对象的值。
4.自定义配置函数
当我们自己在外部定义了一个配置属性,并希望为他定制合并策略方法该怎么做,官方提供了一个配置api
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
也可以直接将内置的合并方法赋值给自定义合并方法:
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.mounted
此外,你也可以改写内置的合并策略方法
Vue.config.optionMergeStrategies.mounted = function(toVal, fromVal) {
// 返回合并后的值
}
组件内部混入 Vue.mixin
组件构造器在生成的时候,有下面一段逻辑(可参考Vue.extend),也会执行mergeOptions,会将Vue的options与组件内部的options进行合并,而mergeOptions内部有一段逻辑,遍历组件内部的mixins,依次调用mergeOptions,最终子组件的options上面就进行了合并配置的操作了,并将合并后的options结果返回给组件构造器的options上。
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
)
// 若child上面存在mixins属性,依次遍历mixins数组,合并到Vue.options上面
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
// parent是父对象的options
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
总结
通过以上的分析: 前文我们提出的两个问题我们就已经清楚了:
1.全局混入后为什么在所有的组件中都能访问到混入的这个属性?
- 因为全局混入的对象合并在Vue的options中,而所有组件构造器生成的过程中,都会合并Vue.options,所以全局混入后,所有的组件中都能访问到混入的属性。
2.当组件中也存在与混入对象上相同的属性时,该怎么处理呢?
- 从上文分析中可以看出,不同的属性有不同的合并策略,同时我们在外部也可以重写vue内部的合并策略,也可以自定义新增属性的合并策略,通过Vue.config.optionMergeStrategies[key]这个属性去配置。
转载自:https://juejin.cn/post/7092627867263041549