在UE(用户体验)设计中,Mixin(混入)是一种设计模式和方法,用于实现代码的复用和功能的扩展。Mixin 的基本概念是将一个或多个已定义的功能集合(例如方法、样式、状态等)注入到一个对象或组件中,以增强其功能而无需改变其原有结构。
一、mixin是什么
Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类。
Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂。
Vue中的mixin
看一下官方定义:
mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods 、created、computed等等。
我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来。
在Vue中我们可以局部混入跟全局混入
局部混入
定义一个mixin对象,有组件options的data、methods属性
var myMixin = {created: function () {this.hello()},methods: {hello: function () {console.log('hello from mixin!')}}
}
组件通过mixins属性调用mixin对象
Vue.component('componentA',{mixins: [myMixin]
})
该组件在使用的时候,混合了mixin里面的方法,在自动执行create生命钩子,执行hello方法
全局混入
通过 Vue.mixin() 进行全局的混入
Vue.mixin({created: function () {console.log("全局混入")}
})
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
全局混入常用于插件的编写
注意事项:
-
当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项
-
但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子
二、使用场景
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立
这时,可以通过Vue的mixin功能将相同或者相似的代码提出来
举例:定义一个modal弹窗组件,内部通过isShowing来控制显示
const Modal = {template: '#modal',data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
定义一个tooltip提示框,内部通过isShowing来控制显示
const Tooltip = {template: '#tooltip',data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了
首先抽出共同代码,编写一个mixin
const toggle = {data() {return {isShowing: false}},methods: {toggleShow() {this.isShowing = !this.isShowing;}}
}
两个组件在使用上,只需要引入mixin
const Modal = {template: '#modal',mixins: [toggle]
};const Tooltip = {template: '#tooltip',mixins: [toggle]
}
通过上面例子,让我们知道mixin对于封装一些可复用的功能如此有趣、方便、实用。
三、源码分析
首先从Vue.mixin入手
export function initMixin (Vue: GlobalAPI) {Vue.mixin = function (mixin: Object) {this.options = mergeOptions(this.options, mixin)return this}
}
主要是调用merOptions方法
export function mergeOptions (parent: Object,child: Object,vm?: Component
): Object {if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并for (let i = 0, l = child.mixins.length; i < l; i++) {parent = mergeOptions(parent, child.mixins[i], vm)}
}const options = {} let keyfor (key in parent) {mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并}for (key in child) {if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了mergeField(key) // 处理child中的key 也就parent中没有处理过的key}}function mergeField (key) {const strat = strats[key] || defaultStratoptions[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并}return options
}
从上面的源码,我们得到以下几点:
- 优先递归处理 mixins
- 先遍历合并parent 中的key,调用mergeField方法进行合并,然后保存在变量options
- 再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options
- 通过 mergeField 函数进行了合并
下面是关于Vue的几种类型的合并策略
- 替换型
- 合并型
- 队列型
- 叠加型
替换型
替换型合并有props、methods、inject、computed
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string
): ?Object {if (!parentVal) return childVal // 如果parentVal没有值,直接返回childValconst ret = Object.create(null) // 创建一个第三方对象 retextend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中return ret
}
strats.provide = mergeDataOrFn
同名的props、methods、inject、computed会被后来者代替
合并型
和并型合并有:data
strats.data = function(parentVal, childVal, vm) { return mergeDataOrFn(parentVal, childVal, vm)
};function mergeDataOrFn(parentVal, childVal, vm) { return function mergedInstanceDataFn() { var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象var parentData = parentVal.call(vm, vm) if (childData) { return mergeData(childData, parentData) // 将2个对象进行合并 } else { return parentData // 如果没有childData 直接返回parentData}}
}function mergeData(to, from) { if (!from) return to var key, toVal, fromVal; var keys = Object.keys(from); for (var i = 0; i < keys.length; i++) {key = keys[i];toVal = to[key];fromVal = from[key]; // 如果不存在这个属性,就重新设置if (!to.hasOwnProperty(key)) {set(to, key, fromVal);} // 存在相同属性,合并对象else if (typeof toVal =="object" && typeof fromVal =="object") {mergeData(toVal, fromVal);}} return to
}
mergeData函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:
- 当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set方法其实就是一些合并重新赋值的方法)
- 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性
队列性
队列性合并有:全部生命周期和watch
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
})// 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
};
生命周期钩子和watch被合并为一个数组,然后正序遍历一次执行
叠加型
叠加型合并有:component、directives、filters
strats.components=
strats.directives=strats.filters = function mergeAssets(parentVal, childVal, vm, key
) { var res = Object.create(parentVal || null); if (childVal) { for (var key in childVal) {res[key] = childVal[key];} } return res
}
叠加型主要是通过原型链进行层层的叠加
通过上述总结出:
- 替换型策略有props、methods、inject、computed,就是将新的同名参数替代旧的参数
- 合并型策略是data, 通过set方法进行合并和重新赋值
- 队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
- 叠加型有component、directives、filters,通过原型链进行层层的叠加
四、Mixin 在 UE 设计中具有以下优点和缺点
优点:
-
代码复用:Mixin 可以将可复用的功能单元定义为独立模块,通过注入到不同的对象或组件中进行复用,避免了重复编写相同的代码,提高了代码的可维护性和重用性。
-
功能扩展:通过注入额外的功能,Mixin 可以扩展对象或组件的能力,使其拥有更多的行为和特性,从而增强了用户体验。
-
灵活组合:Mixin 允许多个功能组合成一个整体,可以根据需要将不同的 Mixin 组合起来,创建具有多种功能的对象或组件,提供更丰富的用户体验。
-
解耦和灵活性:Mixin 的使用可以降低代码间的耦合度,功能与对象解耦,使得代码更加灵活、可测试,便于进行模块化和重构。
缺点(需注意点):
-
命名冲突:使用多个 Mixin 时,如果命名冲突可能会导致代码冲突或覆盖,需要谨慎管理命名,避免命名冲突产生意外结果。
-
复杂性增加:当使用多个 Mixin 时,可能会增加代码的复杂性,特别是在处理多个 Mixin 之间的依赖关系和执行顺序时,需要仔细管理。
-
隐式依赖:Mixin 的使用可能导致代码间的隐式依赖关系,使得代码更加难以理解和维护,特别是在大型项目中。
-
增加理解成本:对于不熟悉 Mixin 的开发人员,理解和使用 Mixin 可能需要额外的学习成本,特别是在处理复杂的 Mixin 组合和冲突解决时。
综上所述,Mixin 在 UE 设计中具有代码复用、功能扩展和灵活组合等优点,但也存在命名冲突、复杂性增加、隐式依赖和理解成本增加等缺点。因此,在使用 Mixin 时需要权衡利弊,并根据具体情况做出合理的选择和管理。