Vue 选项合并详解
什么是选项合并
选项合并是 Vue 将来自不同来源的配置选项(options)合并成一个最终配置的过程。这个过程发生在 Vue 实例创建阶段,通过 mergeOptions 函数完成。
合并的具体内容
Vue 需要合并的选项包括:
- 数据选项:data、props、computed、methods、watch
- DOM 选项:el、template、render、renderError
- 生命周期钩子:created、mounted、updated 等
- 资源选项:components、directives、filters
- 组合选项:mixins、extends
- 其他选项:name、delimiters、comments、inheritAttrs 等
合并的具体场景
1. 创建 Vue 根实例
当我们创建 Vue 根实例时,会将用户传入的选项与 Vue 构造函数的默认选项合并:
// Vue.options 包含 Vue 全局配置和全局注册的组件、指令等
const vm = new Vue({
el: '#app',
data: { message: 'Hello' }
})
// 内部会执行
vm.$options = mergeOptions(
Vue.options, // 构造函数默认选项
{ el: '#app', data: { message: 'Hello' } }, // 用户选项
vm // 实例自身
)
2. 创建组件实例
创建组件实例时,需要合并:
- 组件构造函数选项(通过 Vue.extend 创建)
- 创建实例时传入的选项
- 父组件传递的 props、事件等
// 定义组件
const Component = Vue.extend({
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
}
})
// 创建组件实例
const componentInstance = new Component({
propsData: { /* ... */ }
})
3. 使用 mixins
Mixins 是复用组件逻辑的一种方式:
const myMixin = {
data() {
return { count: 0 }
},
methods: {
increment() { this.count++ }
}
}
new Vue({
mixins: [myMixin],
data() {
return { message: 'Hello' }
}
})
// 内部合并为
// {
// data: { count: 0, message: 'Hello' },
// methods: { increment() { ... } }
// }
4. 组件继承关系(extends)
Vue 组件可以通过 extends 选项扩展另一个组件:
const BaseComponent = {
methods: {
baseMethod() { /* ... */ }
}
}
const ExtendedComponent = {
extends: BaseComponent,
methods: {
extendedMethod() { /* ... */ }
}
}
// 合并为
// {
// methods: {
// baseMethod() { /* ... */ },
// extendedMethod() { /* ... */ }
// }
// }
5. 全局混入(Vue.mixin)
全局混入会影响之后创建的每个 Vue 实例:
// 添加全局 mixin
Vue.mixin({
created() {
console.log('Global mixin hook called')
}
})
// 之后创建的所有实例都会合并这个选项
选项合并策略
Vue 对不同类型的选项采用不同的合并策略:
1. 生命周期钩子
生命周期钩子会被合并为数组,父选项和子选项的钩子都会被保留,并按照父钩子 -> 子钩子的顺序调用:
// 策略示例
function mergeHook(parentVal, childVal) {
return childVal
? parentVal
? parentVal.concat(childVal) // 两者都存在,合并为数组
: Array.isArray(childVal)
? childVal
: [childVal] // 只有子选项,转为数组
: parentVal // 只有父选项,返回父选项
}
2. 组件、指令和过滤器
这些资源选项使用对象合并策略,子选项优先级高:
// 策略示例
function mergeAssets(parentVal, childVal) {
const res = Object.create(parentVal || null) // 创建原型链
return childVal
? extend(res, childVal) // 子选项扩展到结果对象
: res // 无子选项,返回父选项的副本
}
这种策略利用原型链实现继承,可以在不复制所有属性的情况下访问父选项中的资源。
3. data 选项
data 选项会进行递归合并,子选项中的同名属性会覆盖父选项:
// 简化的策略示例
function mergeData(parentVal, childVal) {
if (!childVal) return parentVal
if (!parentVal) return childVal
// 递归合并对象
const ret = {}
const keys = [...Object.keys(parentVal), ...Object.keys(childVal)]
keys.forEach(key => {
// 子选项优先
if (childVal[key] === undefined) {
ret[key] = parentVal[key]
} else if (
typeof childVal[key] === 'object' &&
typeof parentVal[key] === 'object'
) {
ret[key] = mergeData(parentVal[key], childVal[key]) // 递归合并
} else {
ret[key] = childVal[key] // 子选项覆盖父选项
}
})
return ret
}
4. 默认策略(methods、props、computed 等)
默认策略是子选项优先,完全覆盖父选项中的同名属性:
// 默认策略
function defaultStrat(parentVal, childVal) {
return childVal === undefined
? parentVal // 无子选项,使用父选项
: childVal // 有子选项,子选项覆盖父选项
}
为什么需要选项合并
选项合并机制解决了以下问题:
- 代码复用:通过 mixins 和 extends 复用组件逻辑
- 全局配置:通过 Vue.mixin 设置全局行为
- 组件继承:组件可以继承和扩展其他组件
- 插件系统:插件可以通过 Vue.mixin 注入功能
性能考虑
选项合并是一个相对昂贵的操作,尤其是对于复杂组件和深层嵌套对象。这就是为什么 Vue 专门为内部组件提供了 initInternalComponent 优化函数,避免对每个内部组件实例都执行完整的选项合并过程。
在大型应用中,这种优化可以显著提升组件的实例化速度,因为组件创建是频繁操作,任何小的性能提升都会带来明显的效果。