什么 mixin
mixin, 意为混入。
比如去买冰激凌,我先要一点奶油的,再来点香草的。我就可以吃一个奶油香草的冰激凌。如果再加点草莓,我可以同时吃三个口味的冰激凌。
代码表示
假设把你已有的奶油味的称为 base,把要添加的味道称为 mixins。用 js 伪代码可以这么来写:
constbase={
hasCreamFlavor(){
returntrue;
}
}
constmixins={
hasVanillaFlavor(){
returntrue;
},
hasStrawberryFlavor(){
returntrue;
}
}
functionmergeStrategies(base,mixins){
returnObject.assign({},base,mixins);
}
// newBase 就拥有了三种口味。
constnewBase=mergeStrategies(base,mixins);
注意一下这个 mergeStrategies。
合并策略可以你想要的形式,也就是说你可以自定义自己的策略,这是其一。另外要解决冲突的问题。上面是通过 Object.assign 来实现的,那么 mixins 内的方法会覆盖base 内地内容。如果这不是你期望的结果,可以调换 mixin 和 base 的位置。
组合大于继承 && DRY
想象一下上面的例子用继承如何实现?由于 js 是单继承语言,只能一层层继承。写起来很繁琐。这里就体现了 mixin 的好处。符合组合大于继承的原则。
mixin 内通常是提取了公用功能的代码。而不是每一个地方都写一遍。符合 DRY 原则。
什么是 vue mixin
vue mixin 是针对组件间功能共享来做的。可以对组件的任意部分(生命周期, data等)进行mixin,但不同的 mixin 之后的合并策略不同。在源码分析部分会介绍细节。
组件级 mixin
假设两个功能组件 model 和 tooltip ,他们都有一个显示和关闭的 toggle 动作:
//modal
constModal={
template:'#modal',
data(){
return{
isShowing:false
}
},
methods:{
toggleShow(){
this.isShowing=!this.isShowing;
}
}
}
//tooltip
constTooltip={
template:'#tooltip',
data(){
return{
isShowing:false
}
},
methods:{
toggleShow(){
this.isShowing=!this.isShowing;
}
}
}
可以用 mixin 这么写:
consttoggleMixin={
data(){
return{
isShowing:false
}
},
methods:{
toggleShow(){
this.isShowing=!this.isShowing;
}
}
}
constModal={
template:'#modal',
mixins:[toggleMixin]
};
constTooltip={
template:'#tooltip',
mixins:[toggleMixin],
};
全局 mixin
全局 mixin 会作用到每一个 vue 实例上。所以使用的时候要慎重。通常会用 plugin 来显示的声明用到了那些 mixin。
比如 vuex。我们都知道它在每一个实例上扩展了一个 在任意一个组件内可以调用store。那么他是如何实现的呢?
在 src/mixin.js 内
exportdefaultfunction(Vue){
constversion=Number(Vue.version.split('.')[0])
if(version>=2){
Vue.mixin({beforeCreate:vuexInit})
}else{
//overrideinitandinjectvuexinitprocedure
//for1.xbackwardscompatibility.
const_init=Vue.prototype._init
Vue.prototype._init=function(options={}){
options.init=options.init
?[vuexInit].concat(options.init)
:vuexInit
_init.call(this,options)
}
}
/**
*Vuexinithook,injectedintoeachinstancesinithookslist.
*/
functionvuexInit(){
constoptions=this.$options
//storeinjection
if(options.store){
this.$store=typeofoptions.store==='function'
?options.store()
:options.store
}elseif(options.parent&&options.parent.$store){
this.$store=options.parent.$store
}
}
}
我们看到 在 Vue 2.0 以上版本,通过 Vue.mixin({ beforeCreate: vuexInit })实现了在每一个实例的 beforeCreate 生命周期调用vuexInit 方法。
而 vuexInit 方法则是:在跟节点我们会直接把store 注入,在其他节点则拿父级节点的 store,这样this.$store 永远是你在根节点注入的那个store。
vue mixin 源码实现
在 Vuex 的例子中,我们通过 Vue.mixin({ beforeCreate: vuexInit }) 实现对实例的 $store 扩展。
全局 mixin 注册
我们先看一下 mixin 是如何挂载到原型上的。
在 src/core/index.js 中:
importVuefrom'./instance/index'
import{initGlobalAPI}from'./global-api/index'
initGlobalAPI(Vue)
exportdefaultVue
我们发现有一个 initGlobalAPI。在 src/global-api/index 中:
/*@flow*/
importconfigfrom'../config'
import{initUse}from'./use'
import{initMixin}from'./mixin'
import{initExtend}from'./extend'
import{initAssetRegisters}from'./assets'
import{set,del}from'../observer/index'
import{ASSET_TYPES}from'shared/constants'
importbuiltInComponentsfrom'../components/index'
import{
warn,
extend,
nextTick,
mergeOptions,
defineReactive
}from'../util/index'
exportfunctioninitGlobalAPI(Vue:GlobalAPI){
//config
constconfigDef={}
configDef.get=()=>config
if(process.env.NODE_ENV!=='production'){
configDef.set=()=>{
warn(
'DonotreplacetheVue.configobject,setindividualfieldsinstead.'
)
}
}
Object.defineProperty(Vue,'config',configDef)
//exposedutilmethods.
//NOTE:thesearenotconsideredpartofthepublicAPI-avoidrelyingon
//themunlessyouareawareoftherisk.
Vue.util={
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set=set
Vue.delete=del
Vue.nextTick=nextTick
Vue.options=Object.create(null)
ASSET_TYPES.forEach(type=>{
Vue.options[type+'s']=Object.create(null)
})
//thisisusedtoidentifythe"base"constructortoextendallplain-object
//componentswithinWeex'smulti-instancescenarios.
Vue.options._base=Vue
extend(Vue.options.components,builtInComponents)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
所有全局的方法都在这里注册。我们关注 initMixin 方法,定义在 src/core/global-api/mixin.js:
import{mergeOptions}from'../util/index'
exportfunctioninitMixin(Vue:GlobalAPI){
Vue.mixin=function(mixin:Object){
this.options=mergeOptions(this.options,mixin)
returnthis
}
}
至此我们发现了 Vue 如何挂载全局 mixin。
mixin 合并策略
vuex 通过 beforeCreate Hook 实现为所有 vm 添加 $store 实例。让我们先把 hook 的事情放一边。看一看 beforeCreate 如何实现。
在 src/core/instance/init.js 中:
exportfunctioninitMixin(Vue:Class<Component>){
Vue.prototype._init=function(options?:Object){
//removeunrelatedcode
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm,'beforeCreate')
initInjections(vm)//resolveinjectionsbeforedata/props
initState(vm)
initProvide(vm)//resolveprovideafterdata/props
callHook(vm,'created')
//removeunrelatedcode
if(vm.$options.el){
vm.$mount(vm.$options.el)
}
}
}
我们可以看到在 initRender 完成后,会调用 callHook(vm, 'beforeCreate')。而 init 实在 vue 实例化会执行的。
在 src/core/instance/lifecycle.js 中:
exportfunctioncallHook(vm:Component,hook:string){
//#7573disabledepcollectionwheninvokinglifecyclehooks
pushTarget()
consthandlers=vm.$options[hook]
if(handlers){
for(leti=0,j=handlers.length;i<j;i++){
try{
handlers[i].call(vm)
}catch(e){
handleError(e,vm,`${hook}hook`)
}
}
}
if(vm._hasHookEvent){
vm.$emit('hook:'+hook)
}
popTarget()
}
在对 beforeCreate 执行 callHook 过程中,会先从 vue 实例的 options 中取出所有挂载的 handlers。然后循环调用 call 方法执行所有的 hook:
handlers[i].call(vm)
由此我们可以了解到全局的 hook mixin 会和要 mixin 的组件合并 hook,最后生成一个数组。
回头再看:
import{mergeOptions}from'../util/index'
exportfunctioninitMixin(Vue:GlobalAPI){
Vue.mixin=function(mixin:Object){
this.options=mergeOptions(this.options,mixin)
returnthis
}
}
this.options 默认是 vue 内置的一些 option:

image
mixin 就是你要混入的对象。我们来看一看 mergeOptions。定义在 src/core/util/options.js:
exportfunctionmergeOptions(
parent:Object,
child:Object,
vm?:Component
):Object{
if(process.env.NODE_ENV!=='production'){
checkComponents(child)
}
if(typeofchild==='function'){
child=child.options
}
normalizeProps(child,vm)
normalizeInject(child,vm)
normalizeDirectives(child)
constextendsFrom=child.extends
if(extendsFrom){
parent=mergeOptions(parent,extendsFrom,vm)
}
if(child.mixins){
for(leti=0,l=child.mixins.length;i<l;i++){
parent=mergeOptions(parent,child.mixins[i],vm)
}
}
constoptions={}
letkey
for(keyinparent){
mergeField(key)
}
for(keyinchild){
if(!hasOwn(parent,key)){
mergeField(key)
}
}
functionmergeField(key){
conststrat=strats[key]||defaultStrat
options[key]=strat(parent[key],child[key],vm,key)
}
returnoptions
}
忽略不相干代码我们直接跳到:
for(keyinchild){
if(!hasOwn(parent,key)){
mergeField(key)
}
}
functionmergeField(key){
conststrat=strats[key]||defaultStrat
options[key]=strat(parent[key],child[key],vm,key)
}
此时 child 为 { beforeCreate: vuexInit }。走入到 mergeField 流程。mergeField 采取合并策略。
const strat = strats[key] || defaultStrat,相当于取 strats['beforeCreate'] 的合并策略。定义在同文件的上方:
/**
*Hooksandpropsaremergedasarrays.
*/
functionmergeHook(
parentVal:?Array<Function>,
childVal:?Function|?Array<Function>
):?Array<Function>{
returnchildVal
?parentVal
?parentVal.concat(childVal)
:Array.isArray(childVal)
?childVal
:[childVal]
:parentVal
}
LIFECYCLE_HOOKS.forEach(hook=>{
strats[hook]=mergeHook
})
//src/shared/constants.js
exportconstLIFECYCLE_HOOKS=[
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
在 mergeHook 中的合并策略是把所有的 hook 生成一个函数数组。其他相关策略可以在options 文件中查找(如果是对象,组件本身的会覆盖上层,data 会执行结果,返回在merge,hook则生成数组)。
mixin 早于实例化
mergeOptions 会多次调用,正如其注释所描述的那样:
/**
*Mergetwooptionobjectsintoanewone.
*Coreutilityusedinbothinstantiationandinheritance.
*/
上面介绍了全局 mixin 的流程,我们来看下 实例化部分的流程。在 src/core/instance/init.js 中:
exportfunctioninitMixin(Vue:Class<Component>){
Vue.prototype._init=function(options?:Object){
if(options&&options._isComponent){
//optimizeinternalcomponentinstantiation
//sincedynamicoptionsmergingisprettyslow,andnoneofthe
//internalcomponentoptionsneedsspecialtreatment.
initInternalComponent(vm,options)
}else{
vm.$options=mergeOptions(
resolveConstructorOptions(vm.constructor),
options||{},
vm
)
}
//exposerealself
vm._self=vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm,'beforeCreate')
initInjections(vm)//resolveinjectionsbeforedata/props
initState(vm)
initProvide(vm)//resolveprovideafterdata/props
callHook(vm,'created')
if(vm.$options.el){
vm.$mount(vm.$options.el)
}
}
}
由于 全局 mixin 通常放在最上方。所以一个 vue 实例,通常是内置的 options + 全局 mixin 的 options +用户自定义options,加上合并策略生成最终的 options.
那么对于 hook 来说是[mixinHook, userHook]。mixin 的hook 函数优先于用户自定义的 hook 执行。
local mixin
在 组件中书写 mixin 过程中:
constTooltip={
template:'#tooltip',
mixins:[toggleMixin],
};
在 mergeOptions 的过程中有下面一段代码:
if(child.mixins){
for(leti=0,l=child.mixins.length;i<l;i++){
parent=mergeOptions(parent,child.mixins[i],vm)
}
}
当 tooltip 实例化时,会将对应的参数 merge 到实例中。
定制合并策略
Vue.config.optionMergeStrategies.myOption=function(toVal,fromVal){
//returnmergedVal
}
以上。