Vue2 Mixin 深入分析
1. 基础概念
1.1 什么是 Mixin
Mixin(混入)是 Vue2 中实现代码复用的一种机制,允许我们将组件的可复用功能抽取成一个单独的对象。当一个组件使用 mixin 时,mixin 中的所有选项都会被"混合"到组件本身的选项中。
1.2 基本用法示例
javascript
// 定义一个mixin对象
const myMixin = {
created() {
this.hello();
},
methods: {
hello() {
console.log("mixin的方法被调用");
},
},
};
// 在组件中使用mixin
const Component = {
mixins: [myMixin],
created() {
console.log("组件的created钩子被调用");
},
};
// 输出顺序:
// "mixin的方法被调用"
// "组件的created钩子被调用"
2. Mixin 执行时机分析
2.1 基本执行时机
a) 概念解释
Mixin 的执行实际上发生在 Vue 实例化的过程中,具体是在 new Vue()
调用时的选项合并阶段,而不是在 beforeCreate
钩子中。这个过程是在组件实例化时自动完成的,属于 Vue 的初始化流程的一部分。
b) 源码分析
typescript
// src/core/instance/init.js
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this;
// 合并选项
if (options && options._isComponent) {
// 优化内部组件实例化
// 因为动态选项合并非常慢,而且没有一个内部组件选项需要特殊处理
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
// ... 其他初始化逻辑
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
};
}
c) 执行流程图
2. 源码分析
2.1 全局 Mixin 实现
typescript
// src/core/global-api/mixin.ts
export function initMixin(Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
// 核心实现就是合并选项
this.options = mergeOptions(this.options, mixin);
return this;
};
}
2.2 选项合并策略分析
2.2.1 数据对象合并策略
a) 概念解释
数据对象合并策略是 Vue2 中处理 mixin 和组件之间 data 选项的一种机制。当组件使用 mixin 时,两者的 data 对象需要以特定的规则进行合并,以确保数据的正确性和可预测性。
b) 合并规则
- 基本原则
- 递归合并所有属性
- 组件数据(优先以自己的为主)优先级高于 mixin 数据
- 同名属性以组件为准
- 不同名属性全部保留
- 特殊情况处理
- 对象类型属性进行深度合并
- 非对象类型直接覆盖
- 数组类型以组件数据为准
c) 源码实现分析
typescript
// src/core/util/options.ts
function mergeData(
to: Record<string | symbol, any>,
from: Record<string | symbol, any> | null,
recursive = true
): Record<PropertyKey, any> {
// 1. 如果没有来源对象,直接返回目标对象
if (!from) return to;
let key, toVal, fromVal;
// 2. 获取所有属性键(包括 Symbol)
const keys = hasSymbol
? (Reflect.ownKeys(from) as string[])
: Object.keys(from);
// 3. 遍历每个属性进行合并
for (let i = 0; i < keys.length; i++) {
key = keys[i];
// 跳过 Vue 观察者对象
if (key === "__ob__") continue;
toVal = to[key];
fromVal = from[key];
// 4. 处理不同的合并情况
if (!recursive || !hasOwn(to, key)) {
// 4.1 如果目标对象没有该属性,直接设置
set(to, key, fromVal);
} else if (
// 4.2 如果两个值都是对象,则递归合并
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal);
}
// 4.3 其他情况保持目标对象的值不变
}
return to;
}
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本数据合并
const mixin = {
data() {
return {
name: "mixin",
mixinOnly: "mixin data",
shared: {
from: "mixin",
count: 1
}
};
}
};
const component = {
mixins: [mixin],
data() {
return {
name: "component",
componentOnly: "component data",
shared: {
from: "component",
list: []
}
};
}
};
// 最终结果:
{
name: "component", // 组件优先
mixinOnly: "mixin data", // 保留 mixin 特有属性
componentOnly: "component data", // 保留组件特有属性
shared: { // 对象深度合并
from: "component", // 组件属性优先
count: 1, // 保留 mixin 中的特有属性
list: [] // 保留组件中的特有属性
}
}
f) 多个 Mixin 的合并顺序
javascript
const mixin1 = {
data() {
return {
value: "mixin1",
shared: {
count: 1
}
};
}
};
const mixin2 = {
data() {
return {
value: "mixin2",
shared: {
count: 2
}
};
}
};
const component = {
mixins: [mixin1, mixin2], // 从左到右依次合并
data() {
return {
value: "component",
shared: {
count: 3
}
};
}
};
// 合并过程:
// 1. 先合并 mixin1 和 mixin2
// 2. 再将结果与组件合并
// 最终结果:
{
value: "component", // 组件数据优先
shared: {
count: 3 // 组件数据优先
}
}
2.2.2 生命周期钩子合并策略
a) 概念解释
生命周期钩子合并策略是 Vue2 中处理 mixin 和组件之间生命周期钩子函数的特殊机制。与数据对象的合并不同,生命周期钩子需要保证所有定义的钩子函数都能够被正确执行,且要遵循特定的执行顺序。
b) 合并规则
-
基本原则
- 同名钩子函数会被合并到一个数组中
- 保持调用顺序:mixin 中的钩子函数先执行
- 相同的钩子函数只会被执行一次(去重)
- 支持所有 Vue 生命周期钩子
-
执行顺序
- 全局 mixin 的钩子最先执行
- 父 mixin 的钩子其次执行
- 组件自身的钩子最后执行
c) 源码实现分析
typescript
// src/core/util/options.ts
export function mergeLifecycleHook(
parentVal: Array<Function> | null,
childVal: Function | Array<Function> | null
): Array<Function> | null {
// 1. 构建钩子函数数组
const res = childVal
? parentVal
? parentVal.concat(childVal) // 如果父子都有,则合并数组
: isArray(childVal) // 如果只有子值
? childVal // 如果子值是数组则直接使用
: [childVal] // 否则将子值转换为数组
: parentVal; // 如果没有子值,则使用父值
// 2. 去重处理
return res ? dedupeHooks(res) : res;
}
// 去重函数实现
function dedupeHooks(hooks: any) {
const res: Array<any> = [];
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res;
}
// 注册合并策略
LIFECYCLE_HOOKS.forEach((hook) => {
strats[hook] = mergeLifecycleHook;
});
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本钩子合并
const mixin1 = {
created() {
console.log("mixin1 created");
},
mounted() {
console.log("mixin1 mounted");
},
};
const mixin2 = {
created() {
console.log("mixin2 created");
},
};
const component = {
mixins: [mixin1, mixin2],
created() {
console.log("component created");
},
};
// 执行顺序:
// "mixin1 created"
// "mixin2 created"
// "component created"
// "mixin1 mounted"
f) 多层级 Mixin 示例
javascript
// 全局 mixin
Vue.mixin({
created() {
console.log("global mixin created");
},
});
// 父组件 mixin
const parentMixin = {
created() {
console.log("parent mixin created");
},
};
// 子组件 mixin
const childMixin = {
created() {
console.log("child mixin created");
},
};
// 组件定义
const Child = {
mixins: [parentMixin, childMixin],
created() {
console.log("component created");
},
};
// 执行顺序:
// "global mixin created"
// "parent mixin created"
// "child mixin created"
// "component created"
2.2.3 Methods 合并策略
a) 概念解释
Methods 合并策略是 Vue2 中处理 mixin 和组件之间方法的合并机制。与生命周期钩子不同,methods 采用"覆盖"而不是"合并"的策略,这意味着当组件和 mixin 中存在同名方法时,组件中的方法会完全覆盖 mixin 中的方法。
b) 合并规则
-
基本原则
- 组件的方法优先级高于 mixin 的方法
- 同名方法会被完全覆盖,不会像生命周期钩子那样合并
- 后引入的 mixin 中的方法优先级高于先引入的
- 不同名的方法都会被保留
-
特殊情况
- 方法内部的 this 指向当前组件实例
- 可以通过特殊技巧调用被覆盖的方法
- 支持 ES6 的箭头函数(但需要注意 this 绑定)
c) 源码实现分析
typescript
// src/core/util/options.ts
const strats = config.optionMergeStrategies;
strats.methods = function (
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object | null {
if (!parentVal) return childVal; // 如果没有父值,直接返回子值
const ret = Object.create(null); // 创建空对象作为返回值
extend(ret, parentVal); // 复制父值的所有方法
if (childVal) extend(ret, childVal); // 如果有子值,用子值的方法覆盖父值
return ret;
};
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本方法合并
const mixin = {
methods: {
hello() {
console.log("Hello from mixin");
},
greet() {
console.log("Greet from mixin");
},
},
};
const component = {
mixins: [mixin],
methods: {
hello() {
console.log("Hello from component"); // 这个会覆盖mixin中的hello
},
welcome() {
console.log("Welcome from component");
},
},
};
// 最终结果:
// component.hello() // 输出: "Hello from component"
// component.greet() // 输出: "Greet from mixin"
// component.welcome() // 输出: "Welcome from component"
f) 多个 Mixin 的合并顺序
javascript
const mixin1 = {
methods: {
hello() {
console.log("Hello from mixin1");
},
greet() {
console.log("Greet from mixin1");
},
},
};
const mixin2 = {
methods: {
hello() {
console.log("Hello from mixin2");
},
welcome() {
console.log("Welcome from mixin2");
},
},
};
const component = {
mixins: [mixin1, mixin2], // mixin2 会覆盖 mixin1 的同名方法
methods: {
hello() {
console.log("Hello from component"); // 最终会使用这个
},
},
};
// 最终结果:
// component.hello() // 输出: "Hello from component"
// component.greet() // 输出: "Greet from mixin1"
// component.welcome() // 输出: "Welcome from mixin2"
2.2.4 Computed 属性合并策略
a) 概念解释
Computed 属性合并策略是 Vue2 中处理 mixin 和组件之间计算属性的合并机制。与 methods 类似,computed 属性也采用"覆盖"策略,即组件的计算属性会完全覆盖 mixin 中的同名计算属性。这种策略确保了计算属性的响应式和缓存特性能够正常工作。
b) 合并规则
-
基本原则
- 组件的计算属性优先级高于 mixin 的计算属性
- 同名计算属性会被完全覆盖,不会合并
- 后引入的 mixin 中的计算属性优先级高于先引入的
- 不同名的计算属性都会被保留
- 支持 getter/setter 的完整定义
-
特殊情况
- 计算属性的缓存机制会被保留
- 支持计算属性的依赖收集
- 可以访问 this 上下文
- 支持异步计算属性(但不推荐)
c) 源码实现分析
typescript
// src/core/util/options.ts
const strats = config.optionMergeStrategies;
strats.computed = function (
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object | null {
// 如果没有子值,直接返回父值
if (!childVal) return parentVal;
// 如果没有父值,创建新对象
const ret = Object.create(null);
// 如果有父值,扩展到返回对象
if (parentVal) extend(ret, parentVal);
// 扩展子值到返回对象(覆盖同名属性)
extend(ret, childVal);
return ret;
};
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本计算属性合并
const mixin = {
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
},
greeting() {
return `Hello, ${this.fullName}`;
},
},
};
const component = {
mixins: [mixin],
computed: {
fullName() {
// 这个会覆盖mixin中的fullName
return `${this.title} ${this.firstName} ${this.lastName}`;
},
displayName() {
return `User: ${this.fullName}`;
},
},
};
// 最终结果:
// component.fullName // 返回: "Mr. John Doe"
// component.greeting // 返回: "Hello, Mr. John Doe"
// component.displayName // 返回: "User: Mr. John Doe"
f) 多个 Mixin 的合并顺序
javascript
const mixin1 = {
computed: {
userInfo() {
return `${this.name} (${this.role})`;
},
permissions() {
return this.rolePermissions;
},
},
};
const mixin2 = {
computed: {
userInfo() {
return `${this.name} - ${this.department}`; // 会覆盖mixin1的userInfo
},
department() {
return this.getDepartment();
},
},
};
const component = {
mixins: [mixin1, mixin2],
computed: {
userInfo() {
// 最终会使用这个
return `${this.name} (${this.role}) - ${this.department}`;
},
},
};
// 最终结果:
// component.userInfo // 使用组件的实现
// component.permissions // 来自mixin1
// component.department // 来自mixin2
2.2.5 Watch 属性合并策略
a) 概念解释
Watch 属性合并策略是 Vue2 中处理 mixin 和组件之间侦听器的合并机制。与生命周期钩子类似,watch 采用"合并"而不是"覆盖"的策略,这意味着同名的 watch 处理函数会被收集到一个数组中,并按照定义的顺序依次调用。这种策略确保了所有的侦听器都能正常工作,不会互相覆盖。
b) 合并规则
-
基本原则
- 同名 watch 会被合并到数组中
- mixin 中的 watch 优先执行
- 支持多种 watch 定义方式(函数、对象、字符串方法名)
- 支持深度监听和立即执行选项
-
特殊情况
- 支持对象形式的完整配置
- 可以监听多个属性
- 可以使用点语法监听嵌套属性
- 支持监听数组的变化
c) 源码实现分析
typescript
// src/core/util/options.ts
strats.watch = function (
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object | null {
// 处理 Firefox 的 Object.prototype.watch...
if (!childVal) return Object.create(parentVal || null);
if (!parentVal) return childVal;
const ret = Object.create(null);
extend(ret, parentVal);
for (const key in childVal) {
let parent = ret[key];
const child = childVal[key];
// 如果父值存在,确保转换为数组
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key] = parent
? parent.concat(child) // 合并到现有数组
: Array.isArray(child) // 如果子值是数组
? child // 直接使用
: [child]; // 否则转换为数组
}
return ret;
};
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本 watch 合并
const mixin = {
watch: {
value(newVal, oldVal) {
console.log("Mixin watch - value changed:", newVal);
},
"user.name": {
handler(newVal) {
console.log("Mixin watch - user name changed:", newVal);
},
deep: true,
},
},
};
const component = {
mixins: [mixin],
watch: {
value(newVal, oldVal) {
console.log("Component watch - value changed:", newVal);
},
"user.role"(newVal) {
console.log("Component watch - user role changed:", newVal);
},
},
};
// 当 value 改变时的执行顺序:
// 1. "Mixin watch - value changed: ..."
// 2. "Component watch - value changed: ..."
f) 多个 Mixin 的合并顺序
javascript
const mixin1 = {
watch: {
searchText(newVal) {
console.log("Mixin1 watch - searchText:", newVal);
this.fetchData();
},
},
};
const mixin2 = {
watch: {
searchText(newVal) {
console.log("Mixin2 watch - searchText:", newVal);
this.updateFilters();
},
},
};
const component = {
mixins: [mixin1, mixin2],
watch: {
searchText: {
handler(newVal) {
console.log("Component watch - searchText:", newVal);
this.updateUI();
},
immediate: true,
},
},
};
// searchText 改变时的执行顺序:
// 1. "Mixin1 watch - searchText: ..."
// 2. "Mixin2 watch - searchText: ..."
// 3. "Component watch - searchText: ..."
2.2.6 Props 合并策略
a) 概念解释
Props 合并策略是 Vue2 中处理 mixin 和组件之间属性声明的合并机制。与 methods 和 computed 类似,props 也采用"覆盖"策略,但是会对每个 prop 的配置选项进行深度合并。这种策略确保了组件的属性定义的完整性和正确性,同时保持了类型检查和验证功能。
b) 合并规则
-
基本原则
- 组件的 props 配置优先级高于 mixin
- 同名 prop 会进行配置合并
- 类型定义会被合并而不是覆盖
- 验证规则会被保留和合并
-
特殊情况
- 支持数组和对象两种声明方式
- 默认值可以是值或函数
- 支持多类型声明
- 支持自定义验证函数
c) 源码实现分析
typescript
// src/core/util/options.ts
strats.props = function (
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object | null {
if (!parentVal) return childVal;
const ret = Object.create(null);
extend(ret, parentVal);
if (childVal) {
for (const key in childVal) {
const parent = ret[key];
const child = childVal[key];
// 如果父属性存在,合并配置
if (parent && !Array.isArray(parent)) {
ret[key] = extend({}, parent, child);
} else {
ret[key] = child;
}
}
}
return ret;
};
d) 工作流程图
e) 实际应用示例
javascript
// 1. 基本 props 合并
const mixin = {
props: {
title: String,
value: {
type: Number,
default: 0,
validator: (value) => value >= 0,
},
},
};
const component = {
mixins: [mixin],
props: {
title: {
type: String,
required: true,
default: "Untitled",
},
message: String,
},
};
// 最终结果:
// {
// title: {
// type: String,
// required: true,
// default: 'Untitled'
// },
// value: {
// type: Number,
// default: 0,
// validator: value => value >= 0
// },
// message: String
// }
f) 多个 Mixin 的合并顺序
javascript
const mixin1 = {
props: {
name: String,
age: {
type: Number,
default: 18,
},
},
};
const mixin2 = {
props: {
name: {
type: [String, Number],
required: true,
},
role: String,
},
};
const component = {
mixins: [mixin1, mixin2],
props: {
name: {
type: String,
default: "Anonymous",
},
status: String,
},
};
// 最终结果:
// {
// name: {
// type: String,
// default: 'Anonymous'
// },
// age: {
// type: Number,
// default: 18
// },
// role: String,
// status: String
// }
2.2.7 Components/Directives/Filters 合并策略
a) 概念解释
Components、Directives 和 Filters 的合并策略是 Vue2 中处理资源选项的特殊机制。这些资源选项采用"原型继承"的方式进行合并,这意味着子组件可以访问到父级定义的所有资源,同时也可以覆盖或扩展这些资源。这种策略确保了资源的共享和复用,同时保持了局部定义的灵活性。
b) 合并规则
-
基本原则
- 采用原型继承的方式合并
- 组件的资源定义优先级高于 mixin
- 支持全局和局部注册
- 允许覆盖和扩展已有资源
-
特殊情况
- 全局注册的资源对所有组件可用
- 局部注册的资源仅对当前组件可用
- 支持异步组件
- 支持函数式组件
c) 源码实现分析
typescript
// src/core/util/options.ts
function mergeAssets(
parentVal: Object | null,
childVal: Object | null,
vm: Component | null,
key: string
): Object {
// 创建一个原型指向父值的空对象
const res = Object.create(parentVal || null);
// 如果有子值,则扩展到结果对象
if (childVal) {
process.env.NODE_ENV !== "production" &&
assertObjectType(key, childVal, vm);
return extend(res, childVal);
} else {
return res;
}
}
// 注册资源合并策略
ASSET_TYPES.forEach((type) => {
strats[type + "s"] = mergeAssets;
});
d) 工作流程图
e) 实际应用示例
javascript
// 1. 组件合并示例
const mixin = {
components: {
BaseButton: {
template: '<button class="base-btn"><slot/></button>',
},
BaseInput: {
template: '<input class="base-input" v-bind="$attrs">',
},
},
};
const component = {
mixins: [mixin],
components: {
BaseButton: {
// 覆盖 mixin 中的 BaseButton
template: '<button class="custom-btn"><slot/></button>',
},
CustomComponent: {
template: "<div>Custom Component</div>",
},
},
};
// 2. 指令合并示例
const mixin = {
directives: {
focus: {
inserted(el) {
el.focus();
},
},
},
};
const component = {
mixins: [mixin],
directives: {
highlight: {
bind(el, binding) {
el.style.backgroundColor = binding.value;
},
},
},
};
// 3. 过滤器合并示例
const mixin = {
filters: {
capitalize(value) {
return value.charAt(0).toUpperCase() + value.slice(1);
},
},
};
const component = {
mixins: [mixin],
filters: {
currency(value) {
return `$${value.toFixed(2)}`;
},
},
};
f) 多个 Mixin 的合并顺序
javascript
// 1. 多个 mixin 的组件合并
const mixin1 = {
components: {
ComponentA: {
/* ... */
},
SharedComponent: {
/* version 1 */
},
},
};
const mixin2 = {
components: {
ComponentB: {
/* ... */
},
SharedComponent: {
/* version 2 */
},
},
};
const component = {
mixins: [mixin1, mixin2],
components: {
ComponentC: {
/* ... */
},
SharedComponent: {
/* final version */
},
},
};
// 2. 多个 mixin 的指令合并
const mixin1 = {
directives: {
tooltip: {
/* ... */
},
shared: {
/* version 1 */
},
},
};
const mixin2 = {
directives: {
ripple: {
/* ... */
},
shared: {
/* version 2 */
},
},
};
const component = {
mixins: [mixin1, mixin2],
directives: {
highlight: {
/* ... */
},
shared: {
/* final version */
},
},
};
3. 详细的合并规则解析
3.1 数据对象(data)
javascript
const mixin1 = {
data: {
mixin1Prop: 'mixin1',
shared: 'from mixin1'
}
}
const mixin2 = {
data: {
mixin2Prop: 'mixin2',
shared: 'from mixin2'
}
}
const component = {
mixins: [mixin1, mixin2],
data: {
componentProp: 'component',
shared: 'from component'
}
}
// 最终结果:
{
mixin1Prop: 'mixin1',
mixin2Prop: 'mixin2',
componentProp: 'component',
shared: 'from component' // 组件的数据优先级最高
}
3.2 生命周期钩子
javascript
const mixin1 = {
created() {
console.log("mixin1 created");
},
};
const mixin2 = {
created() {
console.log("mixin2 created");
},
};
const component = {
mixins: [mixin1, mixin2],
created() {
console.log("component created");
},
};
// 执行顺序:
// 1. "mixin1 created"
// 2. "mixin2 created"
// 3. "component created"
3.3 方法、计算属性和监听器
javascript
const mixin1 = {
methods: {
someMethod() {
console.log("mixin1 method");
},
},
computed: {
someComputed() {
return "mixin1 computed";
},
},
watch: {
someData(newVal) {
console.log("mixin1 watch:", newVal);
},
},
};
const component = {
mixins: [mixin1],
methods: {
someMethod() {
console.log("component method");
},
},
computed: {
someComputed() {
return "component computed";
},
},
watch: {
someData(newVal) {
console.log("component watch:", newVal);
},
},
};
// 方法和计算属性:组件的实现会覆盖mixin的实现
// 监听器:两个watch都会被调用,mixin的watch先执行
5. 工作原理图解
5.1 Mixin 合并流程
组件优先] I --> M[合并为数组
Mixin先执行] J --> N[键名冲突
组件优先] K --> O[合并为数组
Mixin先执行] L --> P[最终选项] M --> P N --> P O --> P P --> Q[组件实例化]
5.2 数据合并策略图解
watch] F --> I[methods
computed] G --> J[最终组件] H --> J I --> J