背景
mixins的一般用法
就我自己个人而言,mixins一般用的比较多的就是定义一个混入对象,然后在组件里或者新建的vue对象里使用mixins,用法简单明了。基本上和官网用法一样,这里以官网示例为例:
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
VM1740:8 hello from mixin!
全局使用mixins
直到最近想写一个通用业务(埋点),希望能在公共文件引入,不需要改动其他文件的前提下,比如统计进入页面、离开页面的时间,各个生命周期的时间等等。很不凑巧,我用了全局mixins,才发现原来所有的vue实例都会被统计到,不仅仅是当前页面的vue实例,而是当前页面所有用到的子组件都会被统计到。而全局mixins使用的警告,官网早已经告知过,果然什么都要自己试试才会印象深刻,之前也看到过这段文字警告:
也可以全局注册混入对象。
但是注意使用!
一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。
使用恰当时,则可以为自定义对象注入处理逻辑。
还是古人有智慧,“纸上得来终觉浅,绝知此事要躬行”!
mixin原理
这就引起了我的好奇,全局的mixins为什么会影响到所有的子组件?为此特地下载了最新的vue 源码,2.6.8版本,查看了下源码,才发现有部分语法没见过,原来是flow的语法检查,嗯,很抱歉用了这么久的2.x 版本的vue竟然不知道这个。
mixins文件代码很少,如下:
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
可以看出,其实就是把当前Vue实例的options和传入的mixin合并,再返回。真正的实现是靠mergeOptions函数实现的。
mergeOptions函数实现
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)
//上面的代码可以略过不看,主要是检查各种格式的
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,同时检查parent中是否已经有key即是否合并过
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
}
上面的注释已经标明,首先我们要明确这个函数传进去的两个参数分别是this.options 和 mixin,而mergeOptions函数则实现了递归遍历this.options,然后执行mergeField,返回最终合并的this.options。到这里基本上就是mixin的所有实现了。但是mergeField函数看似简单,实际上是很重要的,我还是很好奇这个函数的实现的。
mergeField函数实现
const strats = config.optionMergeStrategies
//默认的合并策略
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)
}
于是我又在代码里找了找,发现strats这个对象有很多属性,如下:
strats.el = strats.propsData = function (parent, child, vm, key) {
strats.data = function (
strats.watch = function (
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
.....
.....
这个时候就很清晰了,一般我们执行mergeField 里的key基本上就是上面strats的属性了,用的最多的可能就是data、methods、props了。所以如果我们在mixins中用到了data,其本质上就是合并当前vue实例对象里的data和我们传进去的mixin里的data,其他属性也是一样的,只是合并策略还需深入研究。
不得不感叹,vue官网真的没有任何废话,官网其实早已给出了选项合并策略。
- 当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合。比如,数据对象在内部会进行递归合并,在和组件的数据发生冲突时以组件数据优先。
- 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。
而回到代码层面上,如果是key为data的话,代码实现如下:
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
}
组件局部注册的原理
其实到这里,我依然不知道为什么全局mixins会影响到所有的子组件,直到我发现了这段代码:
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
}
};
});
}
直到看到这段代码,终于明白了为何会影响所有子组件了,局部注册组件,本质上其实是Vue.extends().而extend里面最终也会执行mergeOptions()函数。至此,终于明白了全局mixins的影响以及实现原理。不过,extend就不再在这里多聊了,等我下次把extend看明白了,再总结吧。