混入 (mixin) 是 Vue.js 中实现代码复用的一种重要机制,它提供了一种灵活的方式来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被"混合"进入该组件本身的选项。
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log("hello from mixin!")
}
}
}
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component()当组件和混入对象含有同名选项时,Vue 会按照特定的策略进行合并。理解这些策略对于正确使用混入至关重要。
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = {
data: function () {
return {
message: "hello",
foo: "abc"
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: "goodbye",
bar: "def"
}
},
created: function () {
console.log(this.$data)
}
})合并结果:
| 属性 | 来源 | 值 |
|---|---|---|
| message | 组件(优先) | 'goodbye' |
| foo | 混入对象 | 'abc' |
| bar | 组件 | 'def' |
同名钩子函数将合并为一个数组,因此都会被调用。混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log("混入对象的钩子被调用")
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log("组件钩子被调用")
}
})执行顺序:
值为对象的选项(如 methods、components、directives、computed 等)将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log("foo")
},
conflicting: function () {
console.log("from mixin")
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log("bar")
},
conflicting: function () {
console.log("from self")
}
}
})
vm.foo()
vm.bar()
vm.conflicting()| 选项类型 | 合并策略 | 冲突时优先级 |
|---|---|---|
| data | 递归合并 | 组件优先 |
| 生命周期钩子 | 合并为数组,依次执行 | 混入先执行 |
| methods / components / directives | 对象合并 | 组件优先 |
| computed | 对象合并 | 组件优先 |
| watch | 同名键的侦听器合并为数组,依次执行 | 混入先执行 |
| props | 默认策略(整个选项覆盖,不深度合并) | 组件优先 |
注意:Vue 2 没有
emits选项(Vue 3 才引入);props采用默认策略,组件定义了props时会整体覆盖混入中的props。
当组件、局部混入和全局混入同时存在时,合并遵循以下优先级:
合并规则:
注意:
Vue.extend()也使用同样的策略进行合并。
混入也可以进行全局注册。使用时需要格外小心,一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: "hello!"
})全局混入适合用于:
Vue.mixin({
mounted: function () {
console.log("[Global Mixin] Component mounted:", this.$options.name)
},
errorCaptured: function (err, vm, info) {
console.error("[Global Error]", err, info)
return false
}
})警告:谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例(包括第三方组件)。大多数情况下,只应当应用于自定义选项。推荐将其作为插件发布,以避免重复应用混入。
Vue 允许自定义选项的合并策略,通过 Vue.config.optionMergeStrategies 配置。
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
return fromVal || toVal
}Vue.config.optionMergeStrategies.myCustomOption = function (parent, child, vm) {
if (parent && child) {
return Object.assign({}, parent, child)
}
return child || parent
}
var mixin = {
myCustomOption: { theme: "dark" }
}
new Vue({
mixins: [mixin],
myCustomOption: { lang: "zh-CN" },
created: function () {
console.log(this.$options.myCustomOption)
}
})Vue.config.optionMergeStrategies.myMethod = Vue.config.optionMergeStrategies.methods
Vue.config.optionMergeStrategies.myHook = Vue.config.optionMergeStrategies.created组件可以使用多个混入对象,它们会按顺序依次合并:
var mixinA = {
created: function () {
console.log("mixinA created")
}
}
var mixinB = {
created: function () {
console.log("mixinB created")
}
}
new Vue({
mixins: [mixinA, mixinB],
created: function () {
console.log("component created")
}
})执行顺序:
混入对象本身也可以使用其他混入:
var baseMixin = {
methods: {
baseMethod: function () {
console.log("base method")
}
}
}
var extendedMixin = {
mixins: [baseMixin],
methods: {
extendedMethod: function () {
console.log("extended method")
}
}
}var formValidationMixin = {
data: function () {
return {
errors: {},
isValid: false
}
},
methods: {
validate: function (rules) {
this.errors = {}
var valid = true
Object.keys(rules).forEach(
function (field) {
var value = this[field]
var fieldRules = rules[field]
if (fieldRules.required && !value) {
this.errors[field] = fieldRules.message || "此字段必填"
valid = false
}
}.bind(this)
)
this.isValid = valid
return valid
},
hasError: function (field) {
return !!this.errors[field]
},
getError: function (field) {
return this.errors[field]
}
}
}
var LoginForm = {
mixins: [formValidationMixin],
data: function () {
return {
username: "",
password: ""
}
},
methods: {
submit: function () {
if (
this.validate({
username: { required: true, message: "请输入用户名" },
password: { required: true, message: "请输入密码" }
})
) {
}
}
}
}var authMixin = {
computed: {
currentUser: function () {
return this.$store.state.user
},
isLoggedIn: function () {
return !!this.currentUser
}
},
methods: {
checkPermission: function (permission) {
if (!this.isLoggedIn) {
this.$router.push("/login")
return false
}
return this.currentUser.permissions.includes(permission)
},
requireAuth: function () {
if (!this.isLoggedIn) {
this.$router.push("/login")
return false
}
return true
}
},
created: function () {
if (this.$options.requireAuth && !this.isLoggedIn) {
this.$router.push("/login")
}
}
}var apiMixin = {
data: function () {
return {
loading: false,
apiError: null
}
},
methods: {
apiRequest: function (url, options) {
var self = this
this.loading = true
this.apiError = null
return fetch(url, options)
.then(function (response) {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json()
})
.catch(function (error) {
self.apiError = error.message
throw error
})
.finally(function () {
self.loading = false
})
}
}
}| 优点 | 说明 |
|---|---|
| 代码复用 | 将通用逻辑抽离,减少重复代码 |
| 灵活性高 | 可以混入任意组件选项 |
| 易于维护 | 修改混入对象会影响所有使用它的组件 |
| 渐进式使用 | 可以按需引入,不强制使用 |
| 缺点 | 说明 |
|---|---|
| 命名冲突 | 多个混入或组件间可能存在属性/方法冲突 |
| 来源不明确 | 组件中难以区分某个方法来自哪个混入 |
| 调试困难 | 出错时难以追踪问题来源 |
| 隐式依赖 | 组件与混入之间存在隐式耦合 |
var badMixin = {
data: function () {
return {}
},
methods: {},
computed: {},
created: function () {}
}
var goodMixin = {
methods: {
formatDate: function (date) {
return new Date(date).toLocaleDateString()
}
}
}var loggerMixin = {
methods: {
logInfo: function (message) {
console.log("[INFO]", message)
},
logError: function (message) {
console.error("[ERROR]", message)
}
}
}// 不推荐
Vue.mixin({
created: function () {}
})
// 推荐:使用局部混入
var componentMixin = {
created: function () {}
}var dataTableMixin = {
props: {
items: { type: Array, required: true },
columns: { type: Array, required: true }
},
data: function () {
return {
sortBy: null,
sortOrder: "asc"
}
}
}组件自身的 data 优先级更高,会覆盖混入的同名属性。
按照 mixins 数组中的顺序依次执行,最后执行组件自身的钩子。
混入中的 this 指向使用它的组件实例,可以直接访问组件的数据和方法。
可以,混入中的 this 就是组件实例,可以访问 this.$props。
| 特性 | Mixin | Composition API |
|---|---|---|
| 代码组织 | 按选项分散 | 按功能组织 |
| 来源追踪 | 不明确 | 明确 |
| TypeScript 支持 | 较弱 | 完整支持 |
| 逻辑复用 | 依赖合并策略 | 函数式组合 |