Vue中mixins
许多框架都有mixins
的设计模式体现,vue2
也不例外。当多个组件想复用一些功能的时候,组件可以使用混入对象,所有混入对象的选项将被混合
进入该组件本身的选项(一个混入对象可以包含任意组件选项componentOptions
类型)。
mixin对象:
// ./mixins/module1.js
export default {
data() {
return {
clientX: 0,
};
},
created() {
window.addEventListener('mousemove', (e) => {
this.clientX = e.clientX;
});
this.$once('hook:beforeDestoryed', () => {
window.removeEventListener('mousemove');
});
},
};
要混入的组件A
import module1 from '@/mixins/module1';
export default {
name: 'A',
mixins: [module1],
render(h) {
return <div>{this.clientX}</div>;
},
};
要混入的组件B
import module1 from '@/mixins/module1';
export default {
name: 'B',
mixins: [module1],
render(h) {
return <span>{this.clientX}</span>;
},
};
上面当组件A
和B
移动鼠标的时候就能获取鼠标的clienX
, mixins
属性可以让我们把复用的逻辑抽离出来,看起来十分灵活。
mixin的缺点
- 来源不清晰
- 命名空间冲突
- 类型推断困难
来源不清晰
上面组件A
没有声明clientX
, 我们可以推断出应该是来自mixins
的module1
中的,因为mixins
中只有module1
。但是下面代码当A组件引用了多个mixins
的时候还能推断clientX
和clientY
分别来自哪个mixin吗
?显然不能,只能跳转到文件的声明处才知道。能看的出mixins
有来源不清晰的问题。
要混入的组件A
import module1 from '@/mixins/module1';
import module2 from '@/mixins/module2';
export default {
name: 'A',
mixins: [module1, module2],
render(h) {
return <div>{this.clientX} {this.clientY}</div>;
// clientX 到底是来自module1还是moudle2?
// clientY 到底是来自module1还是moudle2?
},
};
命名空间冲突
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
vue官方文档明确指出,当出现同名属性的时候,会出现合并的现象。比如这种, 组件data
声明clientX
会覆盖mixins中
的同名属性(生命周期函数是追加到各自数组,只会叠加,无需担心覆盖问题),这个约定对于开发者来说也许并不困惑,凡事总得讲个优先级。
import module1 from '@/mixins/module1';
import module2 from '@/mixins/module2';
export default {
name: 'A',
mixins: [module1, module2],
data() {
return {
clientX: 0,
}
}
render(h) {
return <div>{this.clientX} {this.clientY}</div>;
// clientX 来自自身data,会覆盖mixins
// clientY 到底是来自module1还是moudle2?
},
};
当我也想给A
组件加一个mixin
的时候, 对于我来说最大的困惑就是我该怎么给属性和方法命名?因为同名的时候会出现合并的情况,因此我必须要保证要混入的每个属性或者方法都是独一无二,这就导致我得逐一查看@/mixins/module1
和@/mixins/module2
的代码逻辑,用于防止出现命名冲突,这无形中增加了开发者的负担。
类型推断困难
当我们使用ts来为代码做类型推断,在vscode
中如果已经做了类型限制,只要把鼠标悬停就能清晰看出这个变量的type
。
当使用mixins
时,因为无法知道clientX
的声明来源,导致推断出clientX
十分困难,正常来说vscode
只会将其视为any
。在ts
大行其道的今天,any
大法会被打。
mixins的替代方案
针对mixins
主要问题是混入多个组件参数时会有来源不清晰
和命名空间冲突
问题,社区有以下两种hack方案一定程度可以作为mixin
的替代品。
- hoc高阶组件
- slot组件
hoc高阶组件
vue
中的高阶组件,可以理解为一个函数入参是一个组件,返回一个全新的组件。
hoc = f(componentOptions) => component
mixins
方案
// ./mixins/module1.js
export default {
data() {
return {
count: 0,
};
},
methods: {
addCount() {
this.count++;
}
},
};
// A.vue
import module1 from '@/mixins/module1';
export default {
name: "A",
mixins: [module1],
render(h) {
return <div @click="this.addCount">{this.count}</div>;
},
};
hoc
方案
withModule
为Comp
传递了count
和addCount
。同时A
组件要做对应的改造。
// ./hoc/withModule1.js
export function withModule1(Comp) {
return {
data() {
return {
count: 0,
};
},
render(h) {
return <Comp count={this.count} addCount={this.addCount}></Comp>
},
methods: {
addCount() {
this.count++;
}
},
}
}
// A.vue
import withModule1 from '@/hoc/withModule1';
export default withModule1({
props: ['count', 'addCount'],
name: "A",
render(h) {
return <div @click="this.addCount">{this.count}</div>;
},
});
上面将A
组件改造成了props
接收count
和addCount
, 可以直接得出是来源于withModule1
中。
这个看似是个好方法,实际上并没有解决问题,hoc
接收任意组件入参,自然避免避免不了出现下面这种情况。
// A.vue
import withModule1 from '@/hoc/withModule1';
import withModule2 from '@/hoc/withModule2';
import withModule3 from '@/hoc/withModule3';
export default withModule3(withModule2(withModule1({
props: ['count', 'addCount'],
name: "A",
render(h) {
return <div @click="this.addCount">{this.count}</div>;
},
})));
嵌套场景,你还能看的出数据来源吗?一下子就把hoc
方案打回了原形,它只是mixins
的一个替代品,并没有解决来源不清晰
和命名空间冲突
问题。
slot组件
slot组件方案
// ./slot/slotModule1.js
export default {
data() {
return {
count: 0,
};
},
render(h) {
const vNodes = this.$scopedSlots.default
? this.$scopedSlots.default({
count: this.count,
addCount: this.addCount,
})
: null;
if (vNodes.length > 1) { // 多根节点
return <div>{vNodes}</div>;
}
return vNodes;
},
methods: {
addCount() {
this.count++;
},
},
};
// A.vue
export default {
name: "A",
props:['count', 'addCount'],
render(h) {
return <div @click="this.addCount">{this.count}</div>;
},
};
// 在父组件中使用A
<template>
<SlotModule1 v-slot="{ count, addCount }">
<A :count="count" :addCount="addCount"></A>
</SlotModule1>
</teamplate>
import SlotModule1 from '@/slot/slotModule1.js';
import A from '@/view/A';
export default {
name: 'AParent',
component: {
SlotModule1,
A
}
}
slot组件的方案可以清晰看到向下传递了什么数据,就算出现多层嵌套结构也很清晰。可以说slot
方案解决了mixins
的一些痛点,也是当前替代mixins
最成熟的方案,缺点是对代码入侵性较大,需要不同程度的改造, 特别是需要混入的属性和方法很多时,会出现代码一定程度臃肿。
多级嵌套
// 在父组件中使用A
<template>
<SlotModule1 v-slot="{ count, addCount }">
<SlotModule2 v-slot="{ clientX }">
<A :count="count" :addCount="addCount" />
<B :clientX="clientX" />
</SlotModule2>
</SlotModule1>
</teamplate>
import SlotModule1 from '@/slot/slotModule1.js';
import SlotModule2 from '@/slot/slotModule2.js';
import A from '@/view/A';
import B from '@/view/B';
export default {
name: 'AParent',
component: {
SlotModule1,
SlotModule2,
A,
B
}
}
总结
大部分情况Vue2的mixins
还是十分好用,一些极端的场景换slot
的方案也是可以的。vue3
的composition-api
已经很好的解决了代码复用
和逻辑跳跃
的问题,如果你还在用vue2,但是想用上composition-api
,vue
很早已经推出了过渡方案, 让你可以用上ref、 defineComponent、watchEffect
等。已经在生产项目亲测,可以放心食用。