Mixin

在日常的项目开发中,有一个很常见的场景,有两个非常相似的组件,它们的基本功能是一样的,但它们之间又存在着足够的差异性,此时用户就像是来到了一个岔路口:我是把它拆分成两个不同的组件呢,还是保留为一个组件,然后通过 props 传值来创造差异性从而进行区分呢?

两种解决方案都不够完美:如果拆分成两个组件,用户就不得不冒着一旦功能变动就要在两个文件中更新代码的风险。反之,太多的 props 传值会很快变得混乱不堪,从而提升维护成本。

Vue 中提供了 Mixin(混入)对象,它可以将这些公共的组件逻辑抽离出来,从而使这些功能类似的组件公用这部分逻辑而不会影响公用之外的逻辑。

Mixin 对象是一个类似 Vue 组件但又不是组件的对象,当组件使用 Mixin 对象时,所有 Mixin 对象的选项将被 “混合” 进入该组件本身的选项,如示例代码 3-7-1 所示。

const myMixin = {
    created() {
        this.hello()
    },
    methods: {
        hello() {
            console.log('hello from mixin!')
        }
    }
}

// 组件引入 Mixin
const componentC = {
    mixins: [myMixin],
    data() {
        return {
            str: 'I am C'
        }
    },
    template: '<span>{{ str }}</span>'
}

const componentD = {
    mixins: [myMixin],
    data() {
        return {
            str: 'I am D'
        }
    },
    template: '<span>{{ str }}</span>'
}

上面的代码中,组件 componentC 和组件 componentD 公用了 myMixin 中的逻辑,可以看到打印出 console.log('hello from mixin!')

Mixin合并

由于 Mixin 和组件有着类似的选项,因此当遇到同名的选项时,需要针对这些选项进行合并,主要分为:

  • data 函数中属性的合并。

  • 生命周期钩子的合并。

  • methodscomponentsdirective 等值为对象的合并。

  • 自定义选项的合并。

每个 Mixin 都可以拥有自己的 data 函数,每个 data 函数都会被调用,并将返回结果合并。在数据的 property 发生冲突时,会以组件自身的数据优先,如示例代码3-7-2所示。

示例代码3-7-2 Mixin data 合并

const myMixin = {
    data() {
        return {
            message: 'hello',
            foo: 'abc'
        }
    }
}
const vm = Vue.createApp({
    mixins: [myMixin],
    data() {
        return {
            message: 'goodbye',
            bar: 'def'
        }
    },
    created() {
        console.log(this.message) // => goodbye
    }
}).mount("#app")

上面的代码中,data 函数中相同的 message 属性会以自身组件的 message 优先合并,覆盖掉 Mixin,从而打印出 goodbye

Mixin 的生命周期钩子和组件自身的生命周期钩子同名时,将会依次调用,先调用 Mixin,再调用组件自身的钩子,如示例代码 3-7-3 所示。

示例代码3-7-3 Mixin 生命周期钩子合并
const myMixin = {
    created() {
        console.log('mixin 对象的钩子被调用')
    }
}
const vm = Vue.createApp({
    mixins: [myMixin],
    created() {
        console.log('组件钩子被调用')
    }
}).mount("#app")

上面的代码中,会优先打印出 console.log('mixin对象的钩子被调用'),然后打印 console.log('组件钩子被调用')

methodscomponentsdirective 等值为对象进行合并时,两个对象如果键名不一样,则合并为同一个对象,如果键名一样,则取组件自身的键值对,如示例代码 3-7-4 所示。

示例代码3-7-4 Mixin其他合并1
const myMixin = {
    methods: {
        foo() {
            console.log('foo')
        },
        conflicting() {
            console.log('from mixin')
        }
    }
}
const vm = Vue.createApp({
    mixins: [myMixin],
    methods: {
        bar() {
            console.log('bar')
        },
        conflicting() {
            console.log('from self')
        }
    }
}).mount("#app")
vm.foo() // => 打印"foo"
vm.bar() // => 打印"bar"
vm.conflicting() // => 打印"from self"

最后,对于自定义选项,是指在组件的第一层级中添加的自定义选项,当然 Mixin 也可以有自己的自定义选项,虽然自定义选项使用得并不多,但是当选项同名时,也可以定义合并策略,如示例代码 3-7-5 所示。

const myMixin = {
    custom: 'hello!'
}
const app = Vue.createApp({
    mixins: [myMixin],
    custom: 'goodbye!',
    created(){
        console.log(this.$options.custom)
    }
})
app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
    // 优先组件自身
    // return fromVal || toVal
    // 优先Mixin
    return toVal || fromVal
}
app.mount("#app")

自定义选项在合并时,默认策略为简单地覆盖已有值,也可以采用 optionMergeStrategies 配置自定义属性的合并方案,fromVal 表示自身,toVal 表示 Mixin,如上面的代码所示,可以通过设置不同的返回值来定义合并策略。

全局Mixin

Mixin 也可以进行全局注册。使用时要格外小心,一旦使用全局 Mixin,它将影响每个之后创建的组件,例如每个子组件,如示例代码 3-7-6 所示。

示例代码 3-7-6 全局 Mixin
<div id="app">
    <test-component />
</div>
const app = Vue.createApp({
    myOption: 'hello!'
})

// 为自定义的选项 'myOption' 注入一个处理器
app.mixin({
    created() {
        const myOption = this.$options.myOption
        if (myOption) {
            console.log(myOption)
        }
    }
})

// 将 myOption 也添加到子组件
app.component('test-component', {
    myOption: 'hello from component!',
    template: '<div></div>'
})

app.mount('#app')

上面的代码中,子组件 <test-component> 会打印出 hello!

Mixin取舍

Vue 2 中,Mixin 是将部分组件逻辑抽象成可重用块的主要工具,在一定程度上解决了多个组件的逻辑公用问题,但是也有几个问题:

  • Mixin 很容易发生冲突:因为每个 Mixin 的属性都被合并到同一个组件中,所以相同的 property 名会冲突。

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

为了解决这些问题,Vue 3 提供了组合式 API,添加了一种通过逻辑关注点组织代码的新方法,从而达到更加极致的逻辑共享和复用,让组件化更加完美,我们会在后面的章节深入讲解。