在vue里面,混入(mixin)是一种特殊的使用方式。一个混入对象可以包含任意的组件选项,可根据需求随意“封装”组件的可复用单元,并在使用时根据一定的策略合并到组件的选项当中,使用时与组件自身选项无异。
官方文档对mixin介绍比较少,不能了解甚少,于是便想研究下源码对它混入做个研究和总结
本文基于Vue源码2.x版本
一、说在前面
在分析mixin之前,先看看两个方法,它们在混入的合并过程中扮演着重要的角色
(1) Vue.extend()
Vue.extend()是基础Vue构造器,参数是一个包含组件选项的对象,可用于显式的扩展组件和混入对象,如
var Component = Vue.extend({
mixins: [myMixin]
})
其实,在组件内部的混入合并也是通过它来完成的
(2) extend()
extend()是对象合并方法,参数是源和目标两个对象,用于对象的合并操作
extend(target, source)
其中source对象将合并到target对象,如果source和target的key值相同,则直接覆盖,否则属性添加;作用类似于Object.assgin()和underscore的_.extend(destination, *sources)
二、合并图示
通过对vue源码的研究,我发现混入对于选项的“合并”并不是一步到位的,而是两两合并,并通过合并策略和优先级向一定的方向逐步进行合并操作,最终才得到合并的结果,就像默认的选项合并策略:
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
其中childVal和parentVal是每一次合并中的两个源和目标对象,比如:vm.options.xx和mixin.options.xx,下面用一个例子和图示进行说明
示例代码:
import Vue from 'vue'
Vue.mixin({
data () {
return {
msg: '全局混入-msg',
msg1: '全局混入1-msg-1',
msg2: '全局混入1-msg-2',
msg4: '全局混入1-msg4',
msg5: '全局混入1-msg5',
site: {
name: '腾讯',
url: 'www.tenant.com',
hahaList: [3, 4],
city: 'shenzhen'
}
}
},
created: function () {
console.log('全局混入1 --- created')
},
methods: {
startMix: function () {
console.log('全局混入1 --- startMix')
}
},
computed: {
getMsg: function () {
return 'getMsg 全局混入1!!'
},
getMsg2: function () {
return 'getMsg2 全局混入1!!'
},
getMsg5: function () {
return 'getMsg5 全局混入1!!'
}
}
})
Vue.mixin({
data () {
return {
msg: '全局混入2-msg',
msg1: '全局混入2-msg-1',
msg4: '全局混入2-msg4',
numList: [4, 5],
site: {
name: 'alibaba',
hahaList: [3, 4],
url: 'www.alibaba.com',
people: 30000
}
}
},
created: function () {
console.log('全局混入2 --- created')
},
methods: {
startMix: function () {
console.log('全局混入2 --- startMix')
},
hello: function () {
console.log('全局混入2 --- hello')
}
},
computed: {
getMsg: function () {
return 'getMsg 全局混入2!!'
},
getMsg2: function () {
return 'getMsg2 全局混入2!!'
}
}
})
var localMix3 = {
data () {
return {
msg: '实例混入3',
msg1: '实例混入3-msg1',
msg2: '实例混入3-msg2',
msg3: '实例混入3-msg3',
site: {
name: '淘宝111',
url: 'www',
country: 'China'
}
}
},
created: function () {
console.log('实例混入3 --- created')
},
methods: {
startMix: function () {
console.log('实例混入3 --- startMix')
},
hello: function () {
console.log('实例混入3 --- hello')
}
},
computed: {
getMsg: function () {
return 'getMsg 实例混入3!!'
},
getMsg2: function () {
return 'getMsg2 实例混入3!!'
},
getMsg3: function () {
return 'getMsg3 实例混入3!!'
},
getMsg4: function () {
return 'getMsg4 实例混入3!!'
}
}
}
var localMix2 = {
data () {
return {
msg: '实例混入2',
msg1: '实例混入2-msg1',
msg2: '实例混入2-msg2',
site: {
name: '淘宝',
url: 'www.taobao.com',
title: 'I am 淘宝'
}
}
},
created: function () {
console.log('实例混入2 --- created')
},
methods: {
startMix: function () {
console.log('实例混入2 --- startMix')
},
hello: function () {
console.log('实例混入2 --- hello')
}
},
computed: {
getMsg: function () {
return 'getMsg 实例混入2!!'
},
getMsg2: function () {
return 'getMsg2 实例混入2!!'
},
getMsg3: function () {
return 'getMsg3 实例混入2!!'
}
}
}
var localMix1 = {
data () {
return {
msg: '实例混入1',
msg1: '实例混入1-msg1',
numList: [2, 3],
site: {
name: '淘宝',
url: 'www.taobao.com',
hahaList: [3, 4]
}
}
},
mixins: [localMix2],
created: function () {
console.log('实例混入1 --- created')
},
methods: {
startMix: function () {
console.log('实例混入1 --- startMix')
},
hello: function () {
console.log('实例混入1 --- hello')
}
},
filters: {
capitalize2: function (value) {
if (!value) return ''
return value + ' 实例混入1!!'
}
},
computed: {
getMsg: function () {
return 'getMsg 实例混入1!!'
},
getMsg2: function () {
return 'getMsg2 实例混入1!!'
}
}
}
export default {
name: 'basicExtend',
data () {
return {
msg: 'basicExtend',
numList: [1, 2],
site: {
name: '百度',
hahaList: [1, 2]
}
}
},
mixins: [localMix3, localMix1],
created: function () {
console.log('basicExtend --- created')
},
methods: {
startMix: function () {
console.log('basicExtend --- startMix')
}
},
computed: {
showData: function () {
return JSON.stringify(this.$data)
},
getMsg: function () {
return this.msg + ' !!!'
}
}
}
其中合并后的data和输出如下,
vm.$data:
{
"msg":"basicExtend",
"numList":[1,2],
"site":{
"name":"百度",
"hahaList":[1,2],
"url":"www.taobao.com",
"title":"I am 淘宝",
"country":"China",
"people":30000,
"city":"shenzhen"
},
"msg1":"实例混入1-msg1",
"msg2":"实例混入2-msg2",
"msg3":"实例混入3-msg3",
"msg4":"全局混入2-msg4",
"msg5":"全局混入1-msg5"
}
computed计算属性:
getMsg: basicExtend !!!
getMsg2: getMsg2 实例混入1!!
getMsg3: getMsg3 实例混入2!!
getMsg4: getMsg4 实例混入3!!
getMsg5: getMsg5 全局混入1!!
下面根据这个例子整理的“合并父子图示”,根据后面的讲解再反过来看为什么合并的结果会是这样。
图示描述了源代码中选项合并方法的入参"父子"关系,其中,
- 全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推
- 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并
- 每个混入也可以包含mixins局部混入数组,mixins先完成合并,本混入的options再进行合并
- 组件options最后完成混入合并
- 先合并的"优先级"低,后合并的"优先级"高,也就是组件的options合并优先级最高
- 不同的选项根据自身的混入策略合并方向不一样,这个在下面会有说
三、选项合并
混入对象的混入是每个对象选项和组件选项的“混入合并”,比如:options.data、options.props等,根据不同选项合并策略的不同,各类选项会以不同的方式和方向进行“合并”操作
(1) el,propsData
合并方向: parent --> child
源码:
src/core/util/options.js
const strats = config.optionMergeStrategies
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)
}
}
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
可见,el , propsData使用的是默认合并策略,默认策略比较简单干脆,以child选项为主,若无则使用parent选项
(2) 生命周期钩子
合并方向: parent --> child
源码:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
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
}
可见,LIFECYCLE_HOOKS会将每个hook合并成一个数组,按照图示从父到子开始一步步链接合并成数组,parent在前,child在后。在钩子触发时,按照数组从头顺序调用触发,所以我们看到调用顺序是这样的,与图示一致:
全局混入hook --> 实例混入hook ... --> 组件实例hook
(3) data
合并方向: parent --> child
源码:
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)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
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 () {
// parentVal作为from,childVal作为to,parent->child方向合并
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) {
// parentVal作为from,childVal作为to,parent->child方向合并
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] // child
fromVal = from[key] // parent
if (!hasOwn(to, key)) {
set(to, key, fromVal) //若to没有此key,添加它
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
//若to有此key,且不等
//若to有此key,值非对象,否则进行深度合并
mergeData(toVal, fromVal)
}
}
return to
}
这里set()方法操作添加key并创建响应属性值
可以看到,data的合并比较复杂,按照图示从父到子开始递归合并,以child为主,比较key规则如下:
- 若child无此key,parent有,直接合并此key
- 若child和parent都有此key,且非object类型,忽略不作为
- 若child和parent都有此key,且为object类型,则递归合并对象
(4) components,directives,filters
合并方向: parent <-- child
源码:
export const 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) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal) // child合并到parent
} else {
return res
}
}
可以看到,components, directives,filters的合并策略比较简单,使用extend方法合并为一个对象,按
照图示从子到父进行合并。
其实这是采用原型链委托的方式在合并时把child的属性委托在parent上,这样在使用的时候,在child上查找,没有的再从parent上找,以此类推,所以child的优先级的更高的。
(5) watch
合并方向: parent --> child
源码:
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) //获取parent选项
for (const key in childVal) {
let parent = ret[key] //获取parent选项值
const child = childVal[key] //获取child选项值
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
//每个wather选项合并为数组
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
可见,watch会将每个watcher合并成一个数组,按照图示从父到子顺序合并。在同名wather属性触发时,按照数组从头顺序调用触发,所以我们看到触发顺序是这样的,与图示一致:
全局混入 --> 实例混入 ... --> 组件实例
(6) props,methods,computed,inject
合并方向: parent <-- child
源码:
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) //合并parent
if (childVal) extend(ret, childVal) //合并child
return ret
}
可以看到,props,methods,computed,inject的合并策略和components比较相似,都是使用extend方法合并为一个对象,按照图示从子到父进行合并,所以在调用查找时child优先级更高。
(7) provide
合并方向: parent --> child
源码:
strats.provide = mergeDataOrFn
provide的合并策略和data类似
选项合并策略
如上,对于mixin和组件的每个选项都有对应的合并策略,你可以像下面这样改变默认的合并策略
const strats = Vue.config.optionMergeStrategies
strats.methods = strats.data
但是对于默认选项不建议这么做,会引起合并错误,虽然提供了手段
对于新增的选项,比如vuex,myVOption,也需要什么合并策略,可以使用现有的策略,比如:
Vue.config.optionMergeStrategies.myVOption = strats.methods
也可以自定义策略
Vue.config.optionMergeStrategies.myVOption = function(
parentVal: ?Object,
childVal: ?Object,
): ?Object {
if (!parentVal) return childVal
if (!childVal) return parentVal
return extend(parentVal, childVal)
}
注意:全局混入将对所有的vue实例有效,尽量不要使用;但可以用于插件的发布,比如发布一个画图插件,并提供若干绘画方法