vue 选项合并策略源码分析

合并规则

  • 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
};


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值