mixin, 意為混入。
代碼表示假設把你已有的奶油味的稱為 base,把要添加的味道稱為 mixins。用 js 偽代碼可以這麼來寫:
const base = {
hasCreamFlavor() {
return true;
}
}
const mixins = {
hasVanillaFlavor() {
return true;
},
hasStrawberryFlavor() {
return true;
}
}
function mergeStrategies(base, mixins) {
return Object.assign({}, base, mixins);
}
// newBase 就擁有了三種口味。
const newBase = mergeStrategies(base, mixins);注意一下這個 mergeStrategies。
合併策略可以你想要的形式,也就是說你可以自定義自己的策略,這是其一。另外要解決衝突的問題。上面是通過 Object.assign 來實現的,那麼 mixins 內的方法會覆蓋base 內的內容。如果這不是你期望的結果,可以調換 mixin 和 base 的位置。
組合大於繼承 && DRY想像一下上面的例子用繼承如何實現?由於 js 是單繼承語言,只能一層層繼承。寫起來很繁瑣。這裡就體現了 mixin 的好處。符合組合大於繼承的原則。
mixin 內通常是提取了公用功能的代碼。而不是每一個地方都寫一遍。符合 DRY 原則。
什麼是 vue mixinvue mixin 是針對組件間功能共享來做的。可以對組件的任意部分(生命周期, data等)進行mixin,但不同的 mixin 之後的合併策略不同。在源碼分析部分會介紹細節。
組件級 mixin假設兩個功能組件 model 和 tooltip ,他們都有一個顯示和關閉的 toggle 動作:
//modal
const Modal = {
template: '#modal',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}
//tooltip
const Tooltip = {
template: '#tooltip',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}可以用 mixin 這麼寫:
const toggleMixin = {
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}
const Modal = {
template: '#modal',
mixins: [toggleMixin]
};
const Tooltip = {
template: '#tooltip',
mixins: [toggleMixin],
};
全局 mixin全局 mixin 會作用到每一個 vue 實例上。所以使用的時候要慎重。通常會用 plugin 來顯示的聲明用到了那些 mixin。
比如 vuex。我們都知道它在每一個實例上擴展了一個
在 src/mixin.js 內
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (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 中:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue我們發現有一個 initGlobalAPI。在 src/global-api/index 中:
/* @flow */
import config from '../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'
import builtInComponents from '../components/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
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)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
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'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}至此我們發現了 Vue 如何掛載全局 mixin。
mixin 合併策略vuex 通過 beforeCreate Hook 實現為所有 vm 添加 $store 實例。讓我們先把 hook 的事情放一邊。看一看 beforeCreate 如何實現。
在 src/core/instance/init.js 中:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// remove unrelated code
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// remove unrelated code
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}我們可以看到在 initRender 完成後,會調用 callHook(vm, 'beforeCreate')。而 init 是在 vue 實例化會執行的。
在 src/core/instance/lifecycle.js 中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 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'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}this.options 默認是 vue 內置的一些 option:
imagemixin 就是你要混入的對象。我們來看一看 mergeOptions。定義在 src/core/util/options.js:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
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)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}忽略不相干代碼我們直接跳到:
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}此時 child 為 { beforeCreate: vuexInit }。走入到 mergeField 流程。mergeField 先取合併策略。
const strat = strats[key] || defaultStrat,相當於取 strats['beforeCreate'] 的合併策略。定義在文件的上方:
/**
* 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
})
// src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]在 mergeHook 中的合併策略是把所有的 hook 生成一個函數數組。其他相關策略可以在options 文件中查找(如果是對象,組件本身的會覆蓋上層,data 會執行結果,返回再merge,hook則生成數組)。
mixin 早於實例化mergeOptions 會多次調用,正如其注釋描述的那樣:
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/上面介紹了全局 mixin 的流程,我們來看下 實例化部分的流程。在 src/core/instance/init.js 中:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/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 過程中:
const Tooltip = {
template: '#tooltip',
mixins: [toggleMixin],
};在 mergeOptions 的過程中有下面一段代碼:
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}當 tooltip 實例化時,會將對應的參數 merge 到實例中。
定製合併策略Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// return mergedVal
}以上。
參考http://techsith.com/mixins-in-javascript/https://vuejs.org/v2/guide/mixins.htmlhttps://css-tricks.com/using-mixins-vue-js/