什么 mixin
mixin, 意为混入。
比如去买冰激凌,我先要一点奶油的,再来点香草的。我就可以吃一个奶油香草的冰激凌。如果再加点草莓,我可以同时吃三个口味的冰激凌。
代码表示
假设把你已有的奶油味的称为 base,把要添加的味道称为 mixins。用 js 伪代码可以这么来写:
1const base = { 2 hasCreamFlavor() { 3 return true; 4 } 5} 6const mixins = { 7 hasVanillaFlavor() { 8 return true; 9 }, 10 hasStrawberryFlavor() { 11 return true; 12 } 13} 14 15function mergeStrategies(base, mixins) { 16 return Object.assign({}, base, mixins); 17} 18// newBase 就拥有了三种口味。 19const newBase = mergeStrategies(base, mixins); 20
注意一下这个 mergeStrategies。
合并策略可以你想要的形式,也就是说你可以自定义自己的策略,这是其一。另外要解决冲突的问题。上面是通过 Object.assign 来实现的,那么 mixins 内的方法会覆盖base 内的内容。如果这不是你期望的结果,可以调换 mixin 和 base 的位置。
组合大于继承 && DRY
想象一下上面的例子用继承如何实现?由于 js 是单继承语言,只能一层层继承。写起来很繁琐。这里就体现了 mixin 的好处。符合组合大于继承的原则。
mixin 内通常是提取了公用功能的代码。而不是每一个地方都写一遍。符合 DRY 原则。
什么是 vue mixin
vue mixin 是针对组件间功能共享来做的。可以对组件的任意部分(生命周期, data等)进行mixin,但不同的 mixin 之后的合并策略不同。在源码分析部分会介绍细节。
组件级 mixin
假设两个功能组件 model 和 tooltip ,他们都有一个显示和关闭的 toggle 动作:
1//modal 2const Modal = { 3 template: '#modal', 4 data() { 5 return { 6 isShowing: false 7 } 8 }, 9 methods: { 10 toggleShow() { 11 this.isShowing = !this.isShowing; 12 } 13 } 14} 15 16//tooltip 17const Tooltip = { 18 template: '#tooltip', 19 data() { 20 return { 21 isShowing: false 22 } 23 }, 24 methods: { 25 toggleShow() { 26 this.isShowing = !this.isShowing; 27 } 28 } 29} 30
可以用 mixin 这么写:
1const toggleMixin = { 2 data() { 3 return { 4 isShowing: false 5 } 6 }, 7 methods: { 8 toggleShow() { 9 this.isShowing = !this.isShowing; 10 } 11 } 12} 13 14const Modal = { 15 template: '#modal', 16 mixins: [toggleMixin] 17}; 18 19const Tooltip = { 20 template: '#tooltip', 21 mixins: [toggleMixin], 22}; 23
全局 mixin
全局 mixin 会作用到每一个 vue 实例上。所以使用的时候要慎重。通常会用 plugin 来显示的声明用到了那些 mixin。
比如 vuex。我们都知道它在每一个实例上扩展了一个 在任意一个组件内可以调用store。那么他是如何实现的呢?
在 src/mixin.js 内
1export default function (Vue) { 2 const version = Number(Vue.version.split('.')[0]) 3 4 if (version >= 2) { 5 Vue.mixin({ beforeCreate: vuexInit }) 6 } else { 7 // override init and inject vuex init procedure 8 // for 1.x backwards compatibility. 9 const _init = Vue.prototype._init 10 Vue.prototype._init = function (options = {}) { 11 options.init = options.init 12 ? [vuexInit].concat(options.init) 13 : vuexInit 14 _init.call(this, options) 15 } 16 } 17 /** 18 * Vuex init hook, injected into each instances init hooks list. 19 */ 20 21 function vuexInit () { 22 const options = this.$options 23 // store injection 24 if (options.store) { 25 this.$store = typeof options.store === 'function' 26 ? options.store() 27 : options.store 28 } else if (options.parent && options.parent.$store) { 29 this.$store = options.parent.$store 30 } 31 } 32} 33
我们看到 在 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 中:
1import Vue from './instance/index' 2import { initGlobalAPI } from './global-api/index' 3 4initGlobalAPI(Vue) 5 6export default Vue 7
我们发现有一个 initGlobalAPI。在 src/global-api/index 中:
1/* @flow */ 2 3import config from '../config' 4import { initUse } from './use' 5import { initMixin } from './mixin' 6import { initExtend } from './extend' 7import { initAssetRegisters } from './assets' 8import { set, del } from '../observer/index' 9import { ASSET_TYPES } from 'shared/constants' 10import builtInComponents from '../components/index' 11 12import { 13 warn, 14 extend, 15 nextTick, 16 mergeOptions, 17 defineReactive 18} from '../util/index' 19 20export function initGlobalAPI (Vue: GlobalAPI) { 21 // config 22 const configDef = {} 23 configDef.get = () => config 24 if (process.env.NODE_ENV !== 'production') { 25 configDef.set = () => { 26 warn( 27 'Do not replace the Vue.config object, set individual fields instead.' 28 ) 29 } 30 } 31 Object.defineProperty(Vue, 'config', configDef) 32 33 // exposed util methods. 34 // NOTE: these are not considered part of the public API - avoid relying on 35 // them unless you are aware of the risk. 36 Vue.util = { 37 warn, 38 extend, 39 mergeOptions, 40 defineReactive 41 } 42 43 Vue.set = set 44 Vue.delete = del 45 Vue.nextTick = nextTick 46 47 Vue.options = Object.create(null) 48 ASSET_TYPES.forEach(type => { 49 Vue.options[type + 's'] = Object.create(null) 50 }) 51 52 // this is used to identify the "base" constructor to extend all plain-object 53 // components with in Weex's multi-instance scenarios. 54 Vue.options._base = Vue 55 56 extend(Vue.options.components, builtInComponents) 57 58 initUse(Vue) 59 initMixin(Vue) 60 initExtend(Vue) 61 initAssetRegisters(Vue) 62} 63
所有全局的方法都在这里注册。我们关注 initMixin 方法,定义在 src/core/global-api/mixin.js:
1import { mergeOptions } from '../util/index' 2 3export function initMixin (Vue: GlobalAPI) { 4 Vue.mixin = function (mixin: Object) { 5 this.options = mergeOptions(this.options, mixin) 6 return this 7 } 8} 9
至此我们发现了 Vue 如何挂载全局 mixin。
mixin 合并策略
vuex 通过 beforeCreate Hook 实现为所有 vm 添加 $store 实例。让我们先把 hook 的事情放一边。看一看 beforeCreate 如何实现。
在 src/core/instance/init.js 中:
1export function initMixin (Vue: Class<Component>) { 2 Vue.prototype._init = function (options?: Object) { 3 // remove unrelated code 4 initLifecycle(vm) 5 initEvents(vm) 6 initRender(vm) 7 callHook(vm, 'beforeCreate') 8 initInjections(vm) // resolve injections before data/props 9 initState(vm) 10 initProvide(vm) // resolve provide after data/props 11 callHook(vm, 'created') 12 13 // remove unrelated code 14 if (vm.$options.el) { 15 vm.$mount(vm.$options.el) 16 } 17 } 18} 19
我们可以看到在 initRender 完成后,会调用 callHook(vm, 'beforeCreate')。而 init 实在 vue 实例化会执行的。
在 src/core/instance/lifecycle.js 中:
1export function callHook (vm: Component, hook: string) { 2 // #7573 disable dep collection when invoking lifecycle hooks 3 pushTarget() 4 const handlers = vm.$options[hook] 5 if (handlers) { 6 for (let i = 0, j = handlers.length; i < j; i++) { 7 try { 8 handlers[i].call(vm) 9 } catch (e) { 10 handleError(e, vm, `${hook} hook`) 11 } 12 } 13 } 14 if (vm._hasHookEvent) { 15 vm.$emit('hook:' + hook) 16 } 17 popTarget() 18} 19 20
在对 beforeCreate 执行 callHook 过程中,会先从 vue 实例的 options 中取出所有挂载的 handlers。然后循环调用 call 方法执行所有的 hook:
1handlers[i].call(vm) 2
由此我们可以了解到全局的 hook mixin 会和要 mixin 的组件合并 hook,最后生成一个数组。
回头再看:
1import { mergeOptions } from '../util/index' 2 3export function initMixin (Vue: GlobalAPI) { 4 Vue.mixin = function (mixin: Object) { 5 this.options = mergeOptions(this.options, mixin) 6 return this 7 } 8} 9
this.options 默认是 vue 内置的一些 option:

mixin 就是你要混入的对象。我们来看一看 mergeOptions。定义在 src/core/util/options.js:
1export function mergeOptions ( parent: Object, 2 child: Object, 3 vm?: Component): Object { 4 if (process.env.NODE_ENV !== 'production') { 5 checkComponents(child) 6 } 7 8 if (typeof child === 'function') { 9 child = child.options 10 } 11 12 normalizeProps(child, vm) 13 normalizeInject(child, vm) 14 normalizeDirectives(child) 15 const extendsFrom = child.extends 16 if (extendsFrom) { 17 parent = mergeOptions(parent, extendsFrom, vm) 18 } 19 if (child.mixins) { 20 for (let i = 0, l = child.mixins.length; i < l; i++) { 21 parent = mergeOptions(parent, child.mixins[i], vm) 22 } 23 } 24 const options = {} 25 let key 26 for (key in parent) { 27 mergeField(key) 28 } 29 for (key in child) { 30 if (!hasOwn(parent, key)) { 31 mergeField(key) 32 } 33 } 34 function mergeField (key) { 35 const strat = strats[key] || defaultStrat 36 options[key] = strat(parent[key], child[key], vm, key) 37 } 38 return options 39} 40
忽略不相干代码我们直接跳到:
1 for (key in child) { 2 if (!hasOwn(parent, key)) { 3 mergeField(key) 4 } 5 } 6 function mergeField (key) { 7 const strat = strats[key] || defaultStrat 8 options[key] = strat(parent[key], child[key], vm, key) 9 }
此时 child 为 { beforeCreate: vuexInit }。走入到 mergeField 流程。mergeField 先取合并策略。
const strat = strats[key] || defaultStrat,相当于取 strats['beforeCreate'] 的合并策略。定义在通文件的上方:
1/** 2 * Hooks and props are merged as arrays. 3 */ 4function mergeHook ( parentVal: ?Array<Function>, 5 childVal: ?Function | ?Array<Function>): ?Array<Function> { 6 return childVal 7 ? parentVal 8 ? parentVal.concat(childVal) 9 : Array.isArray(childVal) 10 ? childVal 11 : [childVal] 12 : parentVal 13} 14 15LIFECYCLE_HOOKS.forEach(hook => { 16 strats[hook] = mergeHook 17}) 18 19// src/shared/constants.js 20 21export const LIFECYCLE_HOOKS = [ 22 'beforeCreate', 23 'created', 24 'beforeMount', 25 'mounted', 26 'beforeUpdate', 27 'updated', 28 'beforeDestroy', 29 'destroyed', 30 'activated', 31 'deactivated', 32 'errorCaptured' 33] 34
在 mergeHook 中的合并策略是把所有的 hook 生成一个函数数组。其他相关策略可以在options 文件中查找(如果是对象,组件本身的会覆盖上层,data 会执行结果,返回再merge,hook则生成数组)。
mixin 早于实例化
mergeOptions 会多次调用,正如其注释说描述的那样:
1/** 2 * Merge two option objects into a new one. 3 * Core utility used in both instantiation and inheritance. 4 */ 5
上面介绍了全局 mixin 的流程,我们来看下 实例化部分的流程。在 src/core/instance/init.js 中:
1export function initMixin (Vue: Class<Component>) { 2 Vue.prototype._init = function (options?: Object) { 3 if (options && options._isComponent) { 4 // optimize internal component instantiation 5 // since dynamic options merging is pretty slow, and none of the 6 // internal component options needs special treatment. 7 initInternalComponent(vm, options) 8 } else { 9 vm.$options = mergeOptions( 10 resolveConstructorOptions(vm.constructor), 11 options || {}, 12 vm 13 ) 14 } 15 // expose real self 16 vm._self = vm 17 initLifecycle(vm) 18 initEvents(vm) 19 initRender(vm) 20 callHook(vm, 'beforeCreate') 21 initInjections(vm) // resolve injections before data/props 22 initState(vm) 23 initProvide(vm) // resolve provide after data/props 24 callHook(vm, 'created') 25 if (vm.$options.el) { 26 vm.$mount(vm.$options.el) 27 } 28 } 29} 30
由于 全局 mixin 通常放在最上方。所以一个 vue 实例,通常是内置的 options + 全局 mixin 的 options +用户自定义options,加上合并策略生成最终的 options.
那么对于 hook 来说是[mixinHook, userHook]。mixin 的hook 函数优先于用户自定义的 hook 执行。
local mixin
在 组件中书写 mixin 过程中:
1const Tooltip = { 2 template: '#tooltip', 3 mixins: [toggleMixin], 4}; 5
在 mergeOptions 的过程中有下面一段代码:
1 if (child.mixins) { 2 for (let i = 0, l = child.mixins.length; i < l; i++) { 3 parent = mergeOptions(parent, child.mixins[i], vm) 4 } 5 }
当 tooltip 实例化时,会将对应的参数 merge 到实例中。
定制合并策略
1Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) { 2 // return mergedVal 3}
来自:flyyang's Blog
