背景
mixins的一般用法
就我自己個(gè)人而言,mixins一般用的比較多的就是定義一個(gè)混入對(duì)象,然后在組件里或者新建的vue對(duì)象里使用mixins,用法簡(jiǎn)單明了?;旧虾凸倬W(wǎng)用法一樣,這里以官網(wǎng)示例為例:
// 定義一個(gè)混入對(duì)象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定義一個(gè)使用混入對(duì)象的組件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
VM1740:8 hello from mixin!
全局使用mixins
直到最近想寫一個(gè)通用業(yè)務(wù)(埋點(diǎn)),希望能在公共文件引入,不需要改動(dòng)其他文件的前提下,比如統(tǒng)計(jì)進(jìn)入頁(yè)面、離開(kāi)頁(yè)面的時(shí)間,各個(gè)生命周期的時(shí)間等等。很不湊巧,我用了全局mixins,才發(fā)現(xiàn)原來(lái)所有的vue實(shí)例都會(huì)被統(tǒng)計(jì)到,不僅僅是當(dāng)前頁(yè)面的vue實(shí)例,而是當(dāng)前頁(yè)面所有用到的子組件都會(huì)被統(tǒng)計(jì)到。而全局mixins使用的警告,官網(wǎng)早已經(jīng)告知過(guò),果然什么都要自己試試才會(huì)印象深刻,之前也看到過(guò)這段文字警告:
也可以全局注冊(cè)混入對(duì)象。
但是注意使用!
一旦使用全局混入對(duì)象,將會(huì)影響到 所有 之后創(chuàng)建的 Vue 實(shí)例。
使用恰當(dāng)時(shí),則可以為自定義對(duì)象注入處理邏輯。
還是古人有智慧,“紙上得來(lái)終覺(jué)淺,絕知此事要躬行”!
mixin原理
這就引起了我的好奇,全局的mixins為什么會(huì)影響到所有的子組件?為此特地下載了最新的vue 源碼,2.6.8版本,查看了下源碼,才發(fā)現(xiàn)有部分語(yǔ)法沒(méi)見(jiàn)過(guò),原來(lái)是flow的語(yǔ)法檢查,嗯,很抱歉用了這么久的2.x 版本的vue竟然不知道這個(gè)。
mixins文件代碼很少,如下:
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
可以看出,其實(shí)就是把當(dāng)前Vue實(shí)例的options和傳入的mixin合并,再返回。真正的實(shí)現(xiàn)是靠mergeOptions函數(shù)實(shí)現(xiàn)的。
mergeOptions函數(shù)實(shí)現(xiàn)
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)
//上面的代碼可以略過(guò)不看,主要是檢查各種格式的
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
//處理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) {
//這里parent是this.options
mergeField(key)
}
for (key in child) {
//這里child是mixins,同時(shí)檢查parent中是否已經(jīng)有key即是否合并過(guò)
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
}
上面的注釋已經(jīng)標(biāo)明,首先我們要明確這個(gè)函數(shù)傳進(jìn)去的兩個(gè)參數(shù)分別是this.options 和 mixin,而mergeOptions函數(shù)則實(shí)現(xiàn)了遞歸遍歷this.options,然后執(zhí)行mergeField,返回最終合并的this.options。到這里基本上就是mixin的所有實(shí)現(xiàn)了。但是mergeField函數(shù)看似簡(jiǎn)單,實(shí)際上是很重要的,我還是很好奇這個(gè)函數(shù)的實(shí)現(xiàn)的。
mergeField函數(shù)實(shí)現(xiàn)
const strats = config.optionMergeStrategies
//默認(rèn)的合并策略
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
于是我又在代碼里找了找,發(fā)現(xiàn)strats這個(gè)對(duì)象有很多屬性,如下:
strats.el = strats.propsData = function (parent, child, vm, key) {
strats.data = function (
strats.watch = function (
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
.....
.....
這個(gè)時(shí)候就很清晰了,一般我們執(zhí)行mergeField 里的key基本上就是上面strats的屬性了,用的最多的可能就是data、methods、props了。所以如果我們?cè)趍ixins中用到了data,其本質(zhì)上就是合并當(dāng)前vue實(shí)例對(duì)象里的data和我們傳進(jìn)去的mixin里的data,其他屬性也是一樣的,只是合并策略還需深入研究。
不得不感嘆,vue官網(wǎng)真的沒(méi)有任何廢話,官網(wǎng)其實(shí)早已給出了選項(xiàng)合并策略。
- 當(dāng)組件和混入對(duì)象含有同名選項(xiàng)時(shí),這些選項(xiàng)將以恰當(dāng)?shù)姆绞交旌稀1热?,?shù)據(jù)對(duì)象在內(nèi)部會(huì)進(jìn)行遞歸合并,在和組件的數(shù)據(jù)發(fā)生沖突時(shí)以組件數(shù)據(jù)優(yōu)先。
- 值為對(duì)象的選項(xiàng),例如 methods, components 和 directives,將被混合為同一個(gè)對(duì)象。兩個(gè)對(duì)象鍵名沖突時(shí),取組件對(duì)象的鍵值對(duì)。
而回到代碼層面上,如果是key為data的話,代碼實(shí)現(xiàn)如下:
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)
}
如果key是 methods, components 和 directives
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
}
組件局部注冊(cè)的原理
其實(shí)到這里,我依然不知道為什么全局mixins會(huì)影響到所有的子組件,直到我發(fā)現(xiàn)了這段代碼:
function initAssetRegisters (Vue) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(function (type) {
Vue[type] = function (
id,
definition
) {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (type === 'component') {
validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;
return definition
}
};
});
}
直到看到這段代碼,終于明白了為何會(huì)影響所有子組件了,局部注冊(cè)組件,本質(zhì)上其實(shí)是Vue.extends().而extend里面最終也會(huì)執(zhí)行mergeOptions()函數(shù)。至此,終于明白了全局mixins的影響以及實(shí)現(xiàn)原理。不過(guò),extend就不再在這里多聊了,等我下次把extend看明白了,再總結(jié)吧。