Vue2中mixins

1,596 阅读1分钟

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>;
    },
};

上面当组件AB移动鼠标的时候就能获取鼠标的clienX, mixins属性可以让我们把复用的逻辑抽离出来,看起来十分灵活。

mixin的缺点

  • 来源不清晰
  • 命名空间冲突
  • 类型推断困难

来源不清晰

上面组件A没有声明clientX, 我们可以推断出应该是来自mixinsmodule1中的,因为mixins中只有module1。但是下面代码当A组件引用了多个mixins的时候还能推断clientXclientY分别来自哪个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

image.png

当使用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方案 withModuleComp传递了countaddCount。同时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接收countaddCount, 可以直接得出是来源于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的方案也是可以的。vue3composition-api已经很好的解决了代码复用逻辑跳跃的问题,如果你还在用vue2,但是想用上composition-apivue很早已经推出了过渡方案, 让你可以用上ref、 defineComponent、watchEffect等。已经在生产项目亲测,可以放心食用。

传送门: github.com/vuejs/compo…