合并规则
- Vue针对每个规定的选项都有定义好的合并策略,例如data,component,mounted等。如果合并的子父配置都具有相同的选项,则只需要按照规定好的策略进行选项合并即可。
- 选项不存在默认的合并策略,处理的原则是有子类配置选项则默认使用子类配置选项,没有则选择父类配置选项。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin();
==========================================================
export function initMixin (Vue: Class<Component>) {
// 在原型上添加 _init 方法
Vue.prototype._init = function (options?: Object) {
// 保存当前实例
const vm: Component = this
// 合并配置
if (options && options._isComponent) {
// 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$options
initInternalComponent(vm, options)
} else {
// 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) // 初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化渲染: render, mixin
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) // 初始化 inject
initState(vm) // 初始化组件数据:props, data, methods, watch, computed
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
if (vm.$options.el) {
// 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
// 如果没有传就需要手动执行 $mount 才会进入下一阶段
vm.$mount(vm.$options.el)
}
}
}
mergeOptions
function mergeOptions ( parent, child, vm ) {
···
var options = {};
var key;
//获取父属性
for (key in parent) {
mergeField(key);
}
//获取父属性中没有的子属性
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
//根据传入的属性进行合并
function mergeField (key) {
// 如果有自定义选项策略,则使用自定义选项策略,否则选择使用默认策略。
//获取合并策略
var strat = strats[key] || defaultStrat;
//进行合并
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
defaultStrat
// 用户自定义选项策略
var defaultStrat = function (parentVal, childVal) {
// 子不存在则用父,子存在则用子配置
return childVal === undefined
? parentVal
: childVal
};
el的合并
strats.el = function (parent, child, vm, key) {
if (!vm) { // 只允许vue实例才拥有el属性,其他Vue.extend子类构造器不允许有el属性
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
// 默认策略
return defaultStrat(parent, child)
};
data合并
- 读完这部分源码,可以解开为什么data在vue创建实例时传递的是一个对象,而在组件内部定义时只能传递一个函数
// data的合并
strats.data = function (parentVal, childVal, vm) {
// vm代表是否为Vue创建的实例,否则是Vue.extend创建的子父类的关系(即组件)
if (!vm) { //即Vue.extend创建的
if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象
warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.',vm);
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm); // vue实例时需要传递vm作为函数的第三个参数
};
mergeDataOrFn
- data的合并不是简单的将两个数据对象进行合并,而是直接返回一个mergedDataFn或者mergedInstanceDataFn函数,而真正合并的时机是在后续初始化数据响应式系统的环节进行的,初始化数据响应式系统的第一步就是拿到合并后的数据,也就是执行mergeData逻辑
function mergeDataOrFn ( parentVal, childVal, vm ) {
// 子父类
if (!vm) {
if (!childVal) { // 子类不存在data选项,则合并结果为父类data选项
return parentVal
}
if (!parentVal) { // 父类不存在data选项,则合并结果为子类data选项
return childVal
}
return function mergedDataFn () { // data选项在父类和子类同时存在的情况下返回的是一个函数
// 子类实例和父类实例,分别将子类和父类实例中data函数执行后返回的对象传递给mergeData函数做数据合并
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// Vue实例
// vue构造函数实例对象
return function mergedInstanceDataFn () {
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
// 当实例中传递data选项时,将实例的data对象和Vm构造函数上的data属性选项合并
return mergeData(instanceData, defaultData)
} else {
// 当实例中不传递data时,默认返回Vm构造函数上的data属性选项
return defaultData
}
}
}
}
mergeData
function mergeData (to, from) {
//to是子data
if (!from) { return to }
var key, toVal, fromVal;
// Reflect.ownKeys可以拿到Symbol属性
var keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
//子data中缺少父data的某个属性
if (!hasOwn(to, key)) {
// 则将新增的数据加入响应式系统中。
set(to, key, fromVal);
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 处理深层对象,当合并的数据为多层嵌套对象时,需要递归调用mergeData进行比较合并
mergeData(toVal, fromVal);
}
}
return to
}
- 将父类的数据整合到子类的数据选项中, 如若父类数据和子类数据冲突时,保留子类数据。如果对象有深层嵌套,则需要递归调用mergeData进行数据合并
components、 directive、 filter选项合并
- 父类选项将以原型链的形式被处理。子类必须通过原型链才能查找并使用内置组件和内置指令
// 资源选项
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
// 定义资源合并的策略
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets; // 定义默认策略
});
mergeAssets
// 资源选项自定义合并策略
function mergeAssets (parentVal,childVal,vm,key) {
// 创建一个空对象,其__proto__原型指向父类的资源选项。
var res = Object.create(parentVal || null);
if (childVal) {
//校验components,filters,directives选项必须为对象
assertObjectType(key, childVal, vm);
return extend(res, childVal) // 子类选项赋值合并给res对象
} else {
return res
}
}
合并结果
var vm = new Vue({
components: {
componentA: {}
},
directives: {
'v-boom': {}
}
})
console.log(vm.$options.components)
// 根实例的选项和资源默认选项合并后的结果
{
components: {
componentA: {},
__proto__: {
KeepAlive: {}
Transition: {}
TransitionGroup: {}
}
},
directives: {
'v-boom': {},
__proto__: {
'v-show': {},
'v-model': {}
}
}
}
生命周期钩子函数的合并
- 如果子类和父类都拥有相同钩子选项,则将子类选项和父类选项合并。
- 如果父类不存在钩子选项,子类存在时,则以数组形式返回子类钩子选项。
- 当子类不存在钩子选项时,则以父类选项返回。
- 子父合并时,是将子类选项放在数组的末尾,这样在执行钩子时,永远是父类选项优先于子类选项执行。
var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略
});
// 生命周期钩子选项合并策略
function mergeHook (
parentVal,
childVal
) {
// 1.如果子类和父类都拥有钩子选项,则将子类选项和父类选项合并,
// 2.如果父类不存在钩子选项,子类存在时,则以数组形式返回子类钩子选项,
// 3.当子类不存在钩子选项时,则以父类选项返回。
var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal;
return res
? dedupeHooks(res)
: res
}
// 防止多个相同实例钩子选项多次调用
function dedupeHooks (hooks) {
var res = [];
for (var i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res
}
合并结果
var Parent = Vue.extend({
mounted() {
console.log('parent')
}
})
var Child = Parent.extend({
mounted() {
console.log('child')
}
})
var vm = new Child().$mount('#app');
// 输出结果:
parent
child
watch选项合并
- watch选项最终在合并的数组中可以是包含选项的对象,也可以是对应的回调函数,或者方法名
strats.watch = function (parentVal,childVal,vm,key) {
//火狐浏览器在Object的原型上拥有watch方法,这里对这一现象做了兼容
// var nativeWatch = ({}).watch;
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
// 没有子,则默认用父选项
if (!childVal) { return Object.create(parentVal || null) }
{
// 校验watch选项是一个对象
assertObjectType(key, childVal, vm);
}
// 没有父则直接用子选项
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
// 父的选项先转换成数组
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
合并结果
var Parent = Vue.extend({
watch: {
'test': function() {
console.log('parent change')
}
}
})
var Child = Parent.extend({
watch: {
'test': {
handler: function() {
console.log('child change')
}
}
},
data() {
return {
test: 1
}
}
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 输出结果
parent change
child change
props methods inject computed合并
- 果父类不存在选项,则返回子类选项,子类父类都存在时,用子类选项去覆盖父类选项
// 其他选项合并策略
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal,childVal,vm,key) {
if (childVal && "development" !== 'production') {
//校验是否是对象
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal } // 父类不存在该选项,则返回子类的选项
var ret = Object.create(null);
extend(ret, parentVal); //
if (childVal) {
// 子类选项会覆盖父类选项的值
extend(ret, childVal); }
return ret
};