一、前言

在 Vue 2 中,mixin(混合) 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们存在一些题,比如容易冲突、可重用性有限等等。为了解决这些问题,Vue3.x 添加了一种通过逻辑关注点组织代码的新方法:组合式API(Composition API)。尽管在 Vue3.x 之后你可能在开发中不会用到 mixin,但有必要了解一下,毕竟 Vue3 并没有移除 mixin,你仍然可以使用 mixin 的功能。

注意: mixin 需要使用 Option API

二、基础

Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个 mixin 对象可以包含任意组件选项。当组件使用 mixin 对象时,所有 mixin 对象的选项将被“混合”进入该组件本身的选项。

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. // -- create mixin
  4. const myMixin = {
  5. created() {
  6. // @ts-ignore
  7. this.hello();
  8. },
  9. methods: {
  10. hello() {
  11. console.log('Hello from mixin!');
  12. },
  13. },
  14. };
  15. export default defineComponent({
  16. mixins: [myMixin],
  17. });
  18. </script>

调用组件时,输出:Hello from mixin!

三、选项合并

当组件和 mixin 对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. // -- create mixin
  4. const myMixin = {
  5. data() {
  6. return {
  7. name: 'Muzili',
  8. job: 'Senior Front-End Developer',
  9. };
  10. },
  11. };
  12. export default defineComponent({
  13. mixins: [myMixin],
  14. data() {
  15. return {
  16. name: 'Li-HONGYAO',
  17. address: 'Chengdu China',
  18. };
  19. },
  20. created() {
  21. console.log(this.$data);
  22. },
  23. });
  24. </script>
  25. <template>
  26. <div>Hello,Mixins!</div>
  27. </template>

输出结果:

mixins_1.png

同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用。

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. // -- create mixin
  4. const myMixin = {
  5. created() {
  6. console.log('mixin 对象的钩子被调用');
  7. },
  8. };
  9. export default defineComponent({
  10. mixins: [myMixin],
  11. created() {
  12. console.log('组件钩子被调用');
  13. },
  14. });
  15. // => "mixin 对象的钩子被调用"
  16. // => "组件钩子被调用"
  17. </script>

值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. // -- create mixin
  4. const myMixin = {
  5. methods: {
  6. showJob() {
  7. console.log('Senior Front-End Developer.');
  8. },
  9. sayHello() {
  10. console.log('Hello, girl.');
  11. },
  12. },
  13. };
  14. export default defineComponent({
  15. mixins: [myMixin],
  16. methods: {
  17. showEmail() {
  18. console.log('lihy_online@163.com');
  19. },
  20. sayHello() {
  21. console.log('Hello, boy.');
  22. },
  23. },
  24. created() {
  25. this.showJob();
  26. this.showEmail();
  27. this.sayHello();
  28. },
  29. });

输出结果:

mixins_2.png

注意:Vue.extend() 也使用同样的策略进行合并。

四、全局混入

混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。

  1. app.mixin({ /** */ })

注意:请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。

五、合并策略

自定义选项在合并时,默认策略为简单地覆盖已有值。如果想让某个自定义选项以自定义逻辑进行合并,可以在 app.config.optionMergeStrategies 中添加一个函数:

  1. app.config.optionMergeStrategies.customOption = (toVal, fromVal) => {
  2. // return mergedVal
  3. }

合并策略接收在父实例和子实例上定义的该选项的值,分别作为第一个和第二个参数。

六、@3.x 替代方案 - Hooks

在章节开篇前有提到,传统的 mixin 有很多让人诟病的地方,mixin 的深度合并非常隐式,这让代码逻辑更难理解和调试,具体表现为以下几点:

1)容易冲突:因为每个特性的属性都被合并到同一个组件中,组件内同名的属性或方法会把mixins里的覆盖掉。

2)可重用性有限:我们不能向 mixins 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。

3)数据来源不清晰:组件里所使用的mixins里的数据或方法在当前组件代码里搜索不到,易造成错误的解读,比如被当成错误代码或冗余代码而误删。

为了解决这些问题,在 Vue3.x 中,添加了一种通过逻辑关注点组织代码的新方法:组合式API,但是在组合式API中,由于 setup 内部无法访问 this,所以这两者是冲突的,我们只能封装一套全新的方式来使用类似 mixin 的功能,我们称这种全新的方式为 自定义hooks

  • Hooks 其实就一个函数;
  • Hooks 命名通常以 use 开头;

通过一个示例去帮助大家理解如何自定义 Hooks,ok,开始干活儿,接下来我们要定义一个 useCount 的 Hooks,该函数的功能主要是返回 4个运算法方法以及 运算结果,请看示例:

src/hooks/useCount.ts

  1. import { Ref, ref } from 'vue';
  2. export default function useCount(a: Ref<number>, b: Ref<number>) {
  3. // -- 运算结果
  4. const result = ref(0);
  5. // -- 运算方法
  6. const plus = () => (result.value = a.value + b.value);
  7. const minus = () => (result.value = a.value - b.value);
  8. const multiply = () => (result.value = a.value * b.value);
  9. const divide = () => (result.value = a.value / b.value);
  10. // -- 返回值
  11. return { result, plus, minus, multiply, divide };
  12. }

然后我们 在组件中调用它:

src/components/test.vue

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. import useCount from '../hooks/useCount';
  4. const a = ref(0);
  5. const b = ref(0);
  6. const { result, plus, minus, multiply, divide } = useCount(a, b);
  7. </script>
  8. <template>
  9. <p>
  10. <span>a:</span>
  11. <input v-model.number="a" />
  12. </p>
  13. <p>
  14. <span>b:</span>
  15. <input v-model.number="b" />
  16. </p>
  17. <p>
  18. <span>Actions:</span>
  19. <button type="button" @click="plus">+</button>
  20. <button type="button" @click="minus">-</button>
  21. <button type="button" @click="multiply">x</button>
  22. <button type="button" @click="divide">÷</button>
  23. </p>
  24. <p>
  25. <span>Result:</span>
  26. <span>{{ result }}</span>
  27. </p>
  28. </template>
  29. <style scoped>
  30. button {
  31. margin-right: 10px;
  32. }
  33. </style>

演示效果:

hooks.gif

怎么样,是不是很简单呢?其实我们也可以单独定义一个文件,比如 utils/index.ts,然后将一些公共的方法封装在这一个文件中,但我们需要的时候,直接引入调用对应方法即可。