mixin在vue框架中的定位是實現邏輯複用的功能,能夠類比javascript中的混合繼承方式。實現邏輯複用的方式有不少種,好比react提倡的高階組件、hooks
等等,固然,在Vue中官方推薦的且使用頻次最高的仍是mixin。javascript
本篇文章將會探討Vue底層如何實現mixin,且mixin對vue各個配置項是如何處理的,以及混合的順序如何等等問題。vue
組件調用mixin的方式有兩種:java
不管以上使用了哪一種方式,最終調用的都是mergeOptions
這個工具方法。react
以Vue.mixin舉例:api
// 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
}
}
複製代碼
能夠看到mergeOptions同字面意義同樣,將多個options進行合併,生成一個新的options。數組
mergeOptions是vue中比較重要的輔助函數之一,除了在mixin中使用外還在extend、實例化階段使用到:bash
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
}
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製代碼
首先來看一下mergeOptions的主體代碼:框架
// src/core/util/options.js
export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
...
// 規範化props
normalizeProps(child, vm)
// 規範化inject
normalizeInject(child, vm)
// 規範化指令
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
// 未合併的options不帶有_base
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, 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
}
複製代碼
從代碼邏輯上看,mergeOptions主要經歷了兩個步驟:ide
normalize同字面意思同樣,用來規範化屬性,好比props,可使用對象語法,可使用數組語法,而數組又能夠是函數數組或者是字符串數組。因此normalize的做用就是統一將這些不一樣的類型處理成對象類型的格式。函數
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// props是數組類型
// props: [ 'someObjA', 'someObjB' ]
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
// 將-改駝峯命名
name = camelize(val)
// string類型規範化爲 { someObjA: { type: null } }
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
// 對象類型的props
// props: { someObjA: String }
// props: { someObjA: [ Number, String ] }
// props: { someObjA: { type: Number, default: 1 } }
for (const key in props) {
// 若是是純對象形式,如props類型3則直接使用,不然將屬性後面的值做爲type(如String)
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
複製代碼
在normalizeProps中,字符串數組類型的props都會處理成type爲null的類型,這裏要注意的是,props會根據定義的type不一樣,而給傳進來的props給予不一樣的默認值,好比咱們直接在組件模版上寫require這個屬性:
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
if (Array.isArray(inject)) {
for (let i = 0; i < inject.length; i++) {
// 字符串數組處理成 { bar: { from: 'bar' }}格式
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) {
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
複製代碼
inject/provide是Vue 2.2.0版本引入特性,normalizeInject對inject的處理同props過程類似,都是處理成對象類型格式,但不一樣的是,面對對象類型時normalizeInject又作了一層處理:
inject: {
foo: { someProperty: 'bar' }
}
// 處理後
inject: {
'foo': { from: 'foo', someProperty: 'bar' }
}
複製代碼
這裏對象的處理依舊是再次規範化了一下。
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
複製代碼
normalizeDirectives對指令進行了規範化處理,一樣統一處理成了對象類型。咱們知道Vue針對指令,提供了兩種定義但方式:
// 處理前
directives: {
b: function () {
console.log('v-b')
}
}
// 處理後
directives: {
b: {
bind: function(){
console.log('v-b')
},
update: function(){
console.log('v-b')
}
}
}
複製代碼
綜上,props、inject、directive統一處理成了擴展度較高的對象類型格式,而且格式化後的數據會被從新賦值給傳入的第一個參數(這裏是child),以後就是遞歸處理被合併項的extends和mixins成員了,再遞歸合併以前先作了一次_base
的判斷,這裏的_base
指向Vue構造函數,_base
屬性存在於Vue.options
上,因爲組件初始化階段必定會merge Vue options並返回一個新的options,因此被合併的options必定會存在_base
屬性。
以上是進行合併前的數據處理階段,而mixin真正重要的階段實際上是mergeField
階段,咱們知道兩個組件options能夠存在相同的選項,好比都具備methods
對象,但methods
對象掛載的方法可能相同可能不一樣,其它選項也能夠類比。mergeField
的做用就是考慮使用何種策略去處理這些選項,返回咱們須要的配置。
在mergeOptions函數邏輯最後,首先申請了一個新的存儲對象options,將parant與child都通過mergeField處理再合併進options中。
相關代碼:
function mergeField (key) {
// 策略模式-根據key選擇不一樣的處理函數
const strat = strats[key] || defaultStrat
// 調用處理函數,合併兩個選項
options[key] = strat(parent[key], child[key], vm, key)
}
複製代碼
當mixin兩個普通的對象的時候,可使用深度優先去一層一層拷貝比對來合併值,但在vue中,簡單的拷貝賦值並不能知足組件構造函數的需求,還須要將傳進來的配置項進行處理。
所以組件實例化的過程,能夠看做一個工廠,data、props、methods這些能夠看做原材料,通過各個流水線工人的處理加工,就拿到了生產組件所須要的成品,這裏的strat能夠看到是加工原材料的工人。
因此理解mergeOptions的核心其實就是理解strate的過程。
首先來看Vue是如何定義strates的:
/** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. * 選項覆蓋策略是用來合併父選項值與子選項值到最終值的函數 */
const strats = config.optionMergeStrategies
複製代碼
const defaultStrat = function(parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
複製代碼
defaultStrat表示默認的合併策略,childVal是須要處理的選項,當選項值不爲undefined時,直接返回該選項。這也就意味着,那些未命中合併策略的選項將會被child中的選項直接覆蓋。好比parent與child的組件options中均存在 demo: { ... } 屬性,mixin後,parent中的demo不管定義爲什麼值都會被child中的demo覆蓋。
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)
}
複製代碼
對el和propsData處理是直接返回了默認策略,但返回以前加了對vm的判斷,經過看warn能夠得知el和propsData是經過new關鍵字實例化組件纔可使用的屬性。只要未傳入vm變量,就不能聲明這兩個字段。
傳入了vm的場景:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
複製代碼
未傳入vm的場景:
也就是說,子組件和mixin對象不能定義el和propsData這兩個字段。
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)
}
// provide
strats.provide = mergeDataOrFn
複製代碼
合併data的邏輯異常繁瑣,這也是很是必要的,由於data做爲本地組件的狀態管理器,掛載各類類型的狀態,同時須要合併的data類型可能爲對象也多是函數,但返回結果,必須爲函數類型。
data字段的合併策略依舊是首先判斷了子組件data的類型必須爲函數。以後繼續調用了mergeDataOrFn。
export function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// extend或者mixin調用
return function mergedDataFn() {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// new調用
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
}
}
}
}
複製代碼
mergeDataOrFn函數最終返回了一個函數mergedDataFn或者mergedInstanceDataFn,函數值函數賦值給options.data屬性,也就是說data屬性最終會被處理成一個函數(防止引用傳遞),data屬性真正合並的階段放到了組件的初始化階段。但不管是哪一種函數,mergeData拿到都是須要合併的兩個對象。
function mergeData(to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
// Object.keys拿不到不可枚舉的屬性
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...
// __ob__不合並
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
// 若to不存在該屬性,則使用set賦值
set(to, key, fromVal)
} else if (
// 對象類型則深度合併
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
複製代碼
從上面的過程能夠看到真正執行data合併策略的過程在mergeData內部,而該函數倒是在組件初始化階段才調用,這樣作的主要目的實際上是爲了保證data中能夠訪問到props對應的屬性。
provide的合併策略與data相同。
function mergeHook( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> {
// res返回了一個數組
// parentVal在與vue.options合併階段不存在,因此不會命中parentVal.concat方法,則返回[childVal]
// 與以後的options合併的時候,parentVal必定是數組
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
}
// 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch'
LIFECYCLE_HOOKS.forEach(hook => {
// { created(){}} -> {created: [function created(){}]}
strats[hook] = mergeHook
})
複製代碼
mergeHook函數會將parent與child的生命週期鉤子函數合併成數組形式,好比:
parent.options.created = function() {
console.log('parentCreated')
}
child.options.created = function() {
console.log('childCreated')
}
// 合併後
[
{
created: function() {
console.log('parentCreated')
}
},
{
created: function() {
console.log('childCreated')
}
}
]
複製代碼
最後,生命週期的鉤子函數會在callhook
中依次調用。雖然是依次執行的,但關於函數放置的順序有一些須要注意的事項。
回到開頭提到的mixin使用方式:
咱們編寫一個demo來測試一下這兩種方式的生命週期鉤子函數調用的順序:
// a.js
export default {
created() {
console.log('a')
}
}
// b.js
import mixinA from './a.js'
export default {
mixins: [mixinA],
created() {
console.log('b')
}
}
// a
// b
複製代碼
能夠看到調用順序同官網說明的方式同樣。
// a.js
export default {
created() {
console.log('a')
}
}
// b.js
export default {
created() {
console.log('b')
}
}
// index.js
import Vue from 'vue'
import A from './a.js'
import B from './b.js'
const BComponent = Vue.extends('BComponent', B)
B.mixin(A)
new BComponent()
// b
// a
複製代碼
調用順序與在options內掛載mixins方式調用順序相反,這是爲何呢?
這是由於,組件在實例化的初始階段,必定會與Vue.options進行一次mergeOptions,Vue.options並不存在任何生命週期鉤子函數,且mergeOptions內會優先處理組件options上掛載的mixins,因此mixins內的鉤子函數會優先被push到對應生命週期hook的第一位,因此在調用的時候,mixins混入組件的生命週期會優先調用。
而方式二中,首先調用了extends與Vue.options進行了merge,拿到了BComponent,BComponent中的hook數組第一位是B組件中的hook函數,以後再mixin其它任何組件都只能排在B組件的hook後面。
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)
if (childVal) extend(ret, childVal)
return ret
}
複製代碼
props、methods、inject、computed這幾個固定都是對象類型(props與inject會被規範化成對象類型),直接進行拷貝賦值便可,同名的選項會被覆蓋。在組件內部mixins引入混合組件時,因爲宿主組件老是最後處理,因此當混合組件和宿主組件在props、methods、inject、computed中存在同名屬性時,會被宿主組件對應的選項覆蓋。而調用mixin方法混合的方式則相反,這一點須要注意。
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)
} else {
return res
}
}
// component、directive、filter
ASSET_TYPES.forEach(function(type) {
strats[type + 's'] = mergeAssets
})
複製代碼
乍看之下,directives、filters、components與methods等合併策略很類似,惟獨聲明res變量的方式不同,mergeAssets中使用的是const res = Object.create(parentVal || null)
而不是直接覆蓋,這是爲何呢?
咱們知道,vue內部提供了一些內置的指令如v-for、v-if等,和一些內置的組件如KeepAlive、Transition等,咱們能夠直接在組件內部使用他們,但奇怪的是,vue並無顯式的去註冊他們,這實現的關鍵就在const res = Object.create(parentVal || null)
。
這些內置屬性其實就存在Vue.options上,通過mergeAssets處理後會變成:
options = {
...
components: {
...
__proto__: {
...
Transition
}
}
}
複製代碼
這樣的結構。使用對應的選項時,會順着對應選項的原型鏈一層一層向上尋找選項。
實現的很是巧妙,既知足了同名屬性'覆蓋',又能夠內置選項。
// watch掛載的選項不能夠直接進行覆蓋,須要將每一個選項處理成函數數組形式。
strats.watch = function(
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// Firefox中存在原生的Object.prototype.watch函數
// 爲定義watch選項卻訪問到了watch屬性,則重置parentVal與childVal
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
// 若是定義了watch選項
// 將選項的每一個成員處理成數組類型
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
複製代碼
因爲在Firefox中存在原生的Object.prototype.watch函數,在合併watch函數過程當中若是訪問到了原生函數,須要作兼容性處理。
合併watch同合併聲明週期存在類似之處,都是將選項合併爲數組類型。
合併步驟:
轉載請註明出處!感謝!