混入Mixin是Vue中非常常用的一个功能,也非常灵活,可以大大减少Vue组件中的重复功能,提高可维护性。
先看下使用方法:
// 定义一个混入对象
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!"
复制代码
Mixin的优势在于灵活的配置项合并,可以根据业务需求以恰当的方式进行合并,如组件选项合并,全局混入,及自定义合并策略等等。
注意官方教程上有这么几句话:
1.当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项;
2.数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先,同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用;
3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对;
4.请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。
别小看上面上面几点提示,可能关键时候会直接影响到程序性能。分别看看这几句话在源码上如何实现的。
1.当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项;
先看下全局混入如何实现的:
function initGlobalAPI(Vue){
//...此前代码省略
initMixin$1(Vue);
//...此后代码省略
}
复制代码
initGlobalAPI是全局函数,也是全局API的入口,其中就包含了全局混入。
function initMixin$1(Vue) {
//全局混入
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this;
};
}
复制代码
由此可见核心方法在于mergeOptions,那mergeOptions干了些什么:
/**
这里主要干了两件事:
1、检查和序列化选项;
2、合并选项
*/
function mergeOptions(parent, child, vm) {
//检查是否是合法的组件
{
checkComponents(child);
}
if (typeof child === "function") {
child = child.options;
}
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
//这里的_base是Vue对象
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
//mixins是数组,遍历多个混入
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
//返回一个新的配置项
var options = {};
var key;
//优先遍历父组件选项
for (key in parent) {
mergeField(key);
}
//再遍历子组件选项
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
//注意这里的strats对象
function mergeField(key) {
var strat = strats[key] || defaultStrat;//合并策略
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
复制代码
上面代码看起来挺简单,不就是合并对象吗,我们看下上面要注意的strats对象是什么就知道了。
var strats = config.optionMergeStrategies;
复制代码
strats是一个全局变量,用来重写父选项和子选项的合并规则。,它的属性包括组件的所有配置项。
2.数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先,同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用
3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对;
//返回合并后的data
strats.data = function (parentVal, childVal, vm) {
if (!vm) {
if (childVal && typeof childVal !== "function") {
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);
};
复制代码
function mergeDataOrFn(parentVal, childVal, vm) {
if (!vm) {
// Vue.extend合并,都必须是函数
if (!childVal) {
return parentVal;
}
if (!parentVal) {
return childVal;
}
return function mergedDataFn() {
return mergeData(typeof childVal === "function" ? childVal.call(this, this) : childVal, typeof parentVal === "function" ? parentVal.call(this, this) : parentVal);
};
} else {
return function mergedInstanceDataFn() {
// instance merge
var instanceData = typeof childVal === "function" ? childVal.call(vm, vm) : childVal;
var defaultData = typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData);
} else {
return defaultData;
}
};
}
}
复制代码
//这里就干了一件事,递归合并data
function mergeData(to, from) {
if (!from) {
return to;
}
var key, toVal, fromVal;
var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
// in case the object is already observed...
if (key === "__ob__") {
continue;
}
toVal = to[key];
fromVal = from[key];
if (!hasOwn(to, key)) {
set(to, key, fromVal);
} else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal);
}
}
return to;
}
复制代码
//合并props,methods,inject,computed
strats.props = strats.methods = strats.inject = strats.computed = function (parentVal, childVal, vm, key) {
if (childVal && "development" !== "production") {
assertObjectType(key, childVal, vm);
}
if (!parentVal) {
return childVal;
}
var ret = Object.create(null);
extend(ret, parentVal);//注意这里是创建了新对象,然后将父值合并到新对象
if (childVal) {
extend(ret, childVal);//所以是子组件覆盖或拓展父组件属性或方法
}
return ret;
};
复制代码
//合并生命周期钩子函数
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
复制代码
function mergeHook(parentVal, childVal) {
var res = childVal ? (parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal]) : parentVal;
return res ? dedupeHooks(res) : res;
}
复制代码
//合并watch,注意不是覆盖,而是作为数组进行合并
strats.watch = function (parentVal, childVal, vm, key) {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) {
parentVal = undefined;
}
if (childVal === nativeWatch) {
childVal = undefined;
}
/* istanbul ignore if */
if (!childVal) {
return Object.create(parentVal || null);
}
{
assertObjectType(key, childVal, vm);
}
if (!parentVal) {
return childVal;
}
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child];
}
return ret;
};
复制代码
看到这我们应该明白了上面所说的第2、3点,钩子函数实际是存储在一个数组队列
中,这里是父组件钩子函数concat
子组件钩子函数,所以同名钩子函数都会执行,先执行父组件钩子函数,再执行子组件钩子函数。
所以在实际业务中用到全局mixin时候,我们会经常发现有些方法被执行了多次,先后顺序是不一样的。因此也就不难理解前文提到的第4点,谨慎使用全局mixin,因为它会在所有的组件中都会执行,一旦组件非常多就会导致严重的性能问题。举个例子,通常我们会把一个页面分成若干组件,如果一个列表有100个item组件,那么全局混入中的方法至少执行100次以上,如果其中有比较耗性能的方法,那么就会扩大100倍以上。所以当遇到有严重性能问题的时候,不妨看看是不是全局混入导致的。
小结一下
1、Vue混入实际上是各种属性和方法的合并,只是合并策略不一样;
2、混入的时候data,props,computed,methods,inject的合并规则是子组件会拓展或者覆盖父组件的值,优先子组件;
3、钩子函数及watch的合并是数组的合并,所以父组件、子组件都会执行,父组件先执行,所以慎用全局混入,可能会导致严重的性能问题。