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等。已经在生产项目亲测,可以放心食用。