Skip to content
概述
Vue 的混入(mixin)机制将组件选项分散到多个可复用对象中。这些选项最终经由 mergeOptions 汇聚成一个统一的选项对象,再传入组件实例。合并规则由选项类型决定:data 执行递归合并;生命周期钩子拼接为数组;methods、props、computed、inject 采用同名覆盖;components、directives、filters 基于原型链查找。合并仅在组件定义阶段发生一次(如 Vue.extend 或 defineComponent),运行时不会重复执行。
选项对象与 mixin
选项对象是指传给组件构造器的普通对象,可包含 data、methods、computed、watch、生命周期钩子(created、mounted 等)以及 components、directives 等资源。
mixin 本身也是一个选项对象,使用方式有两类:
- 全局 mixin:
Vue.mixin(obj)将obj合并到Vue.options中,影响之后创建的所有组件实例。 - 局部 mixin:在组件选项的
mixins数组中声明,仅对当前组件及其子组件生效。
基本用法
全局 mixin:
js
Vue.mixin({
created() {
console.log('global mixin created')
}
})局部 mixin:
js
const myMixin = {
data() {
return { shared: 1 }
},
methods: {
log() { /* ... */ }
}
}
const Comp = {
mixins: [myMixin],
// ...
}全局 mixin 会合并到 Vue.options,所有后续组件实例在创建时都会带入这些选项。局部 mixin 只作用于声明它的组件,且合并顺序在全局选项之后、组件自身选项之前。
工作原理
mergeOptions 入口
合并逻辑集中在 src/core/util/options.js 的 mergeOptions 函数中。它的执行流程如下:
- 先对
props、inject、directives做规范化(例如将字符串形式的 props 转为对象格式)。 - 若当前组件定义中存在
extends或mixins,则递归调用mergeOptions将其合并到父选项上。extends先合并,随后按数组顺序依次处理mixins。 - 遍历父选项与子选项的每一个键,从策略表
strats中取出对应的合并函数(data、hooks、methods等均有专属策略),未注册的键使用默认策略。
js
export function mergeOptions(parent, child, vm) {
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0; i < child.mixins.length; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
for (let key in parent) { mergeField(key) }
for (let key in child) {
if (!hasOwn(parent, key)) { mergeField(key) }
}
function mergeField(key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}child._base 用于避免对已经合并过的结果重复处理——mergeOptions 返回的对象会被打上 _base 标记。
data 与 provide:返回延迟求值函数
data 和 provide 的合并并不立刻输出最终对象,而是返回一个函数。组件实例化时在 initData 阶段调用该函数,分别执行父、子 data 函数得到原始数据,再通过 mergeData 进行递归深合并。
js
function mergeData(to, from) {
for (let key in from) {
const fromVal = from[key]
const toVal = to[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal) // 响应式新增
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal) // 同 key 且都是普通对象,递归
}
// 否则保留父数据,子数据被忽略
}
return to
}规则概括为:
- 子数据中父数据不存在的 key,通过
Vue.set添加,保证新属性是响应式的。 - 父子两边同一 key 均为普通对象,则递归合并其属性。
- 其余情形(基本类型或某一方不是普通对象)均保留父数据,子数据被静默丢弃。组件自身 data 优先级最高。
由于合并被包装为函数,每次创建组件实例都会重新执行 data 调用和合并流程。若 mixin 的 data 中包含开销较大的计算,每个实例都会支付同等成本。
生命周期钩子:数组拼接
beforeCreate、mounted 等钩子使用 mergeHook 策略:
js
function mergeHook(parentVal, childVal) {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal) ? childVal : [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}多个来源的同一钩子会合并为一个数组。触发顺序为:全局 mixin 钩子 → 局部 mixins(按数组顺序)→ extends 钩子 → 组件自身钩子。
callHook 遍历这个数组逐个同步执行,每个钩子被单独的 try/catch 包裹,因此一个钩子抛错不会阻止后续钩子运行。
methods、props、computed、inject:同名覆盖
这些选项采用的策略相同:先复制父对象的所有属性,再用子对象覆盖同名 key。
js
strats.methods = function(parentVal, childVal) {
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}后合并的同名值胜出,没有任何警告。多个 mixin 定义了相同名称的方法时,仅最后合并的那个方法被保留,其余全部被覆盖。这种静默行为是命名冲突的主要隐患。
components、directives、filters:原型链继承
js
function mergeAssets(parentVal, childVal) {
const res = Object.create(parentVal || null)
if (childVal) {
for (let key in childVal) {
res[key] = childVal[key]
}
}
return res
}结果对象 res 的原型指向父对象,自身持有子对象的键。组件查找资源时先在自身查找,找不到则沿原型链向上。全局注册的组件(存在于 Vue.options.components)对所有子组件可见,局部注册的同名组件可以覆盖全局版本。开发环境中同名覆盖会输出一条警告。
watch:同键合并为数组
不同来源对同一 key 定义的监听器会被拼成一个数组,按合并顺序依次触发,组件自身的 watcher 不会覆盖 mixin 的 watcher。
js
strats.watch = function(parentVal, childVal) {
if (!childVal) return Object.create(parentVal || null)
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (let key in childVal) {
let parent = ret[key]
let child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent ? parent.concat(child) : [child]
}
return ret
}默认策略
未在策略表中注册的选项(如 el、template)使用默认策略:
js
const defaultStrat = function(parentVal, childVal) {
return childVal === undefined ? parentVal : childVal
}只要子选项有值就采用子选项,否则回退到父选项。
全局与局部 mixin 的合并顺序
Vue.mixin 直接调用 mergeOptions:
js
Vue.mixin = function(mixin) {
this.options = mergeOptions(this.options, mixin)
return this
}Vue.options 是整个应用的全局基准配置。后续每个组件实例化时,都以该全局配置为父参数进入 mergeOptions。局部 mixin 的合并链路大致如下:
Vue.options(全局基准)
→ mergeOptions(parent, component.extends)
→ mergeOptions(parent, mixins[0])
→ mergeOptions(parent, mixins[1])
→ mergeOptions(parent, component 自身选项)数组靠后的 mixin 优先级高于前面的,组件自身选项优先级最高。
示例
以下示例展示 data、钩子和 methods 的合并行为:
js
const mixin = {
data() {
return {
user: { name: 'Alice' },
count: 0
}
},
created() {
console.log('mixin created')
},
methods: {
fetch() {
return 'from mixin'
}
}
}
const Comp = {
mixins: [mixin],
data() {
return {
user: { age: 30 },
count: 1
}
},
created() {
console.log('component created')
},
methods: {
fetch() {
return 'from component'
}
}
}合并结果:
data:user对象被递归合并,最终为{ name: 'Alice', age: 30 };count因组件自身定义了同名 key,直接使用组件的值1。- 生命周期钩子:控制台先输出
mixin created,后输出component created。 methods:Comp的fetch覆盖了 mixin 的fetch,调用this.fetch()返回'from component'。
如果 mixin 中定义了组件 data 里不存在的属性,该属性会通过 Vue.set 被添加到实例上,并保持响应式。
注意点
- data 合并发生在组件实例化阶段,每次创建实例都会重新调用 data 函数并合并。mixin 的 data 计算量大时,实例化耗时需纳入考量。
- 生命周期钩子按阵列顺序同步串行执行,前一个钩子抛出错误不会阻断后续钩子。
- methods、props、computed、inject 的同名冲突不会有任何提示,排查命名来源时需要手动追溯所有 mixin 文件。
- watch 对同一 key 的多个定义会累积为数组并全部执行,这与 methods 的覆盖行为不同。
- components、directives、filters 使用原型链继承,子组件可以覆盖父资源,但查找链是透明的。
Vue 3 中的合并变化
Vue 3 保留了对 Options API mixin 的兼容,内部合并流程重写在 packages/runtime-core/src/componentOptions.ts 的 applyOptions 函数中。与 Vue 2 的核心差异在于:
- Vue 2 通过
mergeOptions返回一个新的合并选项对象,所有策略集中在strats对象;Vue 3 不再生成合并结果对象,而是直接在组件实例上逐层 apply 选项。 - 处理顺序:先递归处理
mixins数组中的每个 mixin,再处理extends,最后 apply 组件自身选项。这一顺序保证了优先级与 Vue 2 一致。 - data 合并逻辑仍采用延迟函数,内部行为类似
mergeData,mixin data 中组件没有的属性仍会通过set()挂到实例上。setup()返回的响应式数据会与 options data 分别存放,两者共存但可能因同名属性产生混淆。 - 生命周期钩子依然拼接为数组并按序调用,methods、props、inject 等选项采用同名覆盖;components、directives 基于原型链继承。
- 不再暴露全局策略表
optionMergeStrategies,自定义合并策略需要通过对象方式显式处理。
局限性
mixin 在实际使用中存在几个固有问题:
- 属性来源不透明:阅读组件代码时,无法直接判断某个属性来自哪个 mixin。
- 隐式依赖:mixin 可能依赖组件提供特定 data 或 method,该契约既无声明也无类型检查。若组件重构时删除被依赖的选项,mixin 会静默失效。
- 命名冲突不可控:多个 mixin 定义了同名 method 时,后合并的会覆盖前者,没有错误或警告。项目规模较大时冲突几乎无法避免。
- 无法为引入的属性重命名:这导致某些组合使用变得困难,除非修改源码。
应用场景
尽管存在以上问题,以下场景中 mixin 仍可使用,但需预先评估是否有更清晰的替代方案:
- 已有大量 mixin 基础设施的 Vue 2 项目,可渐进地在新增功能中使用组合式函数,保持旧代码稳定。
- 插件安装时的一次性注入,如
Vue.mixin({ beforeCreate() { ... } }),只在应用启动时执行一次,命名冲突风险较低。 - 非逻辑复用的代码共享,如全局注册组件(
Vue.component),其底层原本就使用原型链式合并策略,适合该场景。
参考链接
- Vue 2
mergeOptions源码:src/core/util/options.js - Vue 2 全局 mixin 实现:
src/core/global-api/mixin.js - Vue 2
mergeDataOrFn:src/core/util/options.js - Vue 2 生命周期钩子合并:
src/core/util/options.js中的mergeHook - Vue 3
applyOptions:packages/runtime-core/src/componentOptions.ts - Vue 3 组件初始化中调用
applyOptions:packages/runtime-core/src/component.ts → setupComponent - 用户自定义合并策略接口:
src/core/config.js中的optionMergeStrategies
