Vue mixins 和 extends 使用详解

Vue 提供了mixins 和 extends 来在组件里直接合并一些特定的属性。
用法如下:

var mixin = {
  created: function () { console.log(1) }
}
var vm = new Vue({
  created: function () { console.log(2) },
  mixins: [mixin]
})
// => 1
// => 2
var CompA = { ... }
var CompB = {
  extends: CompA,
  ...
}

以上两者的用法来自vue官方案例,最大的区别就是extends传入的是一个组件属性对象,mixins传入的是n个组件属性对象组成的数组。

这里我们会有一个疑问,当我们注入想动的组件属性到当前组件,合并的机制是什么?是组件覆盖mixins或者extends 还是后者覆盖前者,还是两者共存。这是本文说明的重点。我们看代码:

mergeField是合并的核心代码,他会根据属性名,在合并方法合集strats里去找一个特定属性key的合并方法。调用合并方法进行合并操作。然后把合并的结果返回给vm实例。

// vue实例在初始化的时候会调用该合并方法
 vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
function mergeOptions (
    parent,
    child,
    vm
  ) {
    {
      checkComponents(child);
    }

    if (typeof child === 'function') {
      child = child.options;
    }

    normalizeProps(child, vm);
    normalizeInject(child, vm);
    normalizeDirectives(child);
    if (!child._base) {
     // 初始先合并extends和mixins的属性到 _base 对象上
      if (child.extends) {
        parent = mergeOptions(parent, child.extends, vm); 
      }
      if (child.mixins) {
        for (var i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], vm);
        }
      }
    }

    var options = {};
    var key;
    // 先处理_base 和mixins 或者extends 混合后的内容
    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
  }

下面将展示针对不同属性的合并策略对应的源代码:

// 这个是属性里data和组件里data的合并方式

strats.data = function (
    parentVal,
    childVal,
    vm
  ) {
    if (!vm) {
      if (childVal && typeof childVal !== 'function') {
        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)
  };

这个是属性里 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);
  // childVal 覆盖 ret里的parentVal
  if (childVal) { extend(ret, childVal); }
  return ret
};

这个是对el和propsData的覆盖方法

strats.el = strats.propsData = function (parent, child, vm, key) {
      if (!vm) {
        warn(
          "option \"" + key + "\" can only be used during instance " +
          'creation with the `new` keyword.'
        );
      }
      return defaultStrat(parent, child)
    };

这是对watch属性的合并方法

strats.watch = function (
    parentVal,
    childVal,
    vm,
    key
  ) {
    // work around Firefox's Object.prototype.watch...
    if (parentVal === nativeWatch) { parentVal = undefined; }
    if (childVal === nativeWatch) { childVal = undefined; }
    /* istanbul ignore if */
    if (!childVal) { return Object.create(parentVal || null) }
    {
      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];
      }
      // 按watch的key合并回掉函数到数组中,供后面顺序执行
      ret[key$1] = parent
        ? parent.concat(child)
        : Array.isArray(child) ? child : [child];
    }
    return ret
  };

这是对provide的合并方法

strats.provide = mergeDataOrFn;

生命周期属性的覆盖方法

  var LIFECYCLE_HOOKS = [
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated',
    'beforeDestroy',
    'destroyed',
    'activated',
    'deactivated',
    'errorCaptured',
    'serverPrefetch'
  ];
LIFECYCLE_HOOKS.forEach(function (hook) {
    strats[hook] = mergeHook;
  });
 
 function mergeHook (
    parentVal,
    childVal
  ) {
    var res = childVal
      ? parentVal
        ? parentVal.concat(childVal)
        : Array.isArray(childVal)
          ? childVal
          : [childVal]
      : parentVal;
    return res
      ? dedupeHooks(res)
      : res
  }

属性中 三种vue内部类型(组件,指令,过滤器)的合并方法

 var ASSET_TYPES = [
    'component',
    'directive',
    'filter'
  ];
ASSET_TYPES.forEach(function (type) {
    strats[type + 's'] = mergeAssets;
  });

上面的多种策略对应的最终合并函数如下:

 // 合并顺序是
 // to里加入只有from中特有的属性,to和from里都有的属性,保留to里的,最后返回to
  function mergeData (to, from) {
    if (!from) { return to }
    var key, toVal, fromVal;

    var keys = hasSymbol
      ? Reflect.ownKeys(from)
      : Object.keys(from);

    for (var i = 0; i < keys.length; i++) {
      key = keys[i];
      // in case the object is already observed...
      if (key === '__ob__') { continue }
      toVal = to[key];
      fromVal = from[key];
      if (!hasOwn(to, key)) {
        set(to, key, fromVal);
      } else if (
        toVal !== fromVal &&
        isPlainObject(toVal) &&
        isPlainObject(fromVal)
      ) {
        mergeData(toVal, fromVal);
      }
    }
    return to
  }
// 合并 vue中data或者reject,他们是一个函数,所以合并函数返回值
function mergeDataOrFn (
    parentVal,
    childVal,
    vm
  ) {
    if (!vm) {
      // in a Vue.extend merge, both should be functions
      if (!childVal) {
        return parentVal
      }
      if (!parentVal) {
        return childVal
      }
      // when parentVal & childVal are both present,
      // we need to return a function that returns the
      // merged result of both functions... no need to
      // check if parentVal is a function here because
      // it has to be a function to pass previous merges.
      return function mergedDataFn () {
        return mergeData(
          typeof childVal === 'function' ? childVal.call(this, this) : childVal,
          typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
        )
      }
    } else {
      return function mergedInstanceDataFn () {
        // instance merge
        var instanceData = typeof childVal === 'function'
          ? childVal.call(vm, vm)
          : childVal;
        var defaultData = typeof parentVal === 'function'
          ? parentVal.call(vm, vm)
          : parentVal;
        if (instanceData) {
          return mergeData(instanceData, defaultData)
        } else {
          return defaultData
        }
      }
    }
  }
// 默认的合并策略,优先使用childVal,childVal没有才使用parentVal
  var defaultStrat = function (parentVal, childVal) {
    return childVal === undefined
      ? parentVal
      : childVal
  };

// to里有的将被_from覆盖,to里没有的form有的,to里将添加。
 function extend (to, _from) {
    for (var key in _from) {
      to[key] = _from[key];
    }
    return to
  }

总结:

属性名称 合并策略 对应合并函数
data mixins/extends 只会将自己有的但是组件上没有内容混合到组件上,重复定义默认使用组件上的
如果data里的值是对象,将递归内部对象继续按照该策略合并
mergeDataOrFn,mergeData
provide 同上 mergeDataOrFn,mergeData
props mixins/extends 只会将自己有的但是组件上没有内容混合到组件上 extend
methods 同上 extend
inject 同上 extend
computed 同上 extend
组件,过滤器,指令属性 同上 extend
el 同上 defaultStrat
propsData 同上 defaultStrat
watch 合并watch监控的回掉方法
执行顺序是先mixins/extends里watch定义的回调,然后是组件的回掉
strats.watch
HOOKS 生命周期钩子 同一种钩子的回调函数会被合并成数组
执行顺序是先mixins/extends里定义的钩子函数,然后才是组件里定义的
mergeHook

mixins和extends 的合并策略都是按照上面的表格来合并的。

你可能感兴趣的:(前端,vue,mixins,extends)