mixin
基础
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
局部混入
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
使用场景
多个组件中使用了同一个变量、方法,为了避免在每个组件中都写一份,提高代码的重用性,可以采用mixin混如到各个组件中使用,方便统一管理。
特点
混入对象可以包含任意组件选项,可以定义Data、methods、Components、LifeCycle Hooks(生命周期函数)、Directives(指令)、路由钩子函数等。
方法和参数在各组件中不共享
如混入对象中有一个 num:1的变量,在组件A中改变num值为5,这时候在组件B中获取这个值,拿到的还是1,还是混入对象里的初始值,数据不共享。
与vuex的区别
- vuex:用来做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。
- mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的,值的修改在组件中不会相互影响。
与公共组件的区别
在父组件中引入组件,相当于在父组件中给出一片独立的空间供子组件使用,然后根据props来传值,但本质上两者是相对独立的。
与extens的区别
extends只能继承一个,mixin可以是多个。
合并顺序
- 全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推
- 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并
- 每个混入也可以包含mixins局部混入数组,mixins先完成合并,本混入的options再进行合并
- 组件options最后完成混入合并
- 先合并的”优先级”低,后合并的”优先级”高,也就是组件的options合并优先级最高
- 不同的选项也会根据自身的混入策略进行合并
mixin合并策略
核心代码
源码位置:src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// ...省略
// 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
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
for (key in parent) {
mergeField(key) // 先遍历parent的key
}
for (key in child) {
// 如果parent已经处理过某个key 就不处理了
if (!hasOwn(parent, key)) {
mergeField(key) // 处理child中的key 也就parent中没有处理过的key
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
}
return options
}
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
-
优先递归处理 mixins
-
先遍历合并 parent 中的key,调用mergeField方法进行合并,然后保存在变量options
-
再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options
-
strats的值如下:
替换型
props、methods、inject、computed属于替换型。
// 源码位置:src/core/util/options.js
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// ...省略
if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
const ret = Object.create(null)
extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
return ret
}
// 源码位置:src/shared/util.js
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
合并型
data属于合并型,我们将源码简写一下。
// 源码位置:src/core/util/options.js
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
// ...省略
return mergeDataOrFn(parentVal, childVal, vm)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
// ...省略
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
}
}
}
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]
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(toVal, fromVal)
}
}
return to
}
- 当目标 data 对象不包含当前属性时,调用 set 方法进行合并。
- 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性。
- 若child无此key,parent有,直接合并此key。
- 若child和parent都有此key,且非object类型,忽略不作为。
- 若child和parent都有此key,且为object类型,则递归合并对象。
队列型
生命周期函数钩子属于队列型,先进先出。
// 源码位置:src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
// 源码位置:src/core/util/options.js
import { LIFECYCLE_HOOKS } from 'shared/constants'
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
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
}
Vue 实例的生命周期钩子被合并为一个数组,然后正序遍历一次执行。
总结
- 全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推
- 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并
- 每个混入也可以包含mixins局部混入数组,mixins先完成合并,本混入的options再进行合并
- 组件options最后完成混入合并
- 不同的类型走不同的合并策略
- 替换型策略:props、methods、inject、computed, 就是将新的同名参数替代旧的参数。
- 合并型策略:data,通过set方法进行合并和重新赋值。
- 队列型策略:生命周期函数,原理是将函数存入一个数组,然后正序遍历依次执行。