你真的理解Vue的混入吗

混入Mixin是Vue中非常常用的一个功能,也非常灵活,可以大大减少Vue组件中的重复功能,提高可维护性。
先看下使用方法:

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"
复制代码

Mixin的优势在于灵活的配置项合并,可以根据业务需求以恰当的方式进行合并,如组件选项合并,全局混入,及自定义合并策略等等。
注意官方教程上有这么几句话:

1.当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项;
2.数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先,同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用;
3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对;
4.请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。

别小看上面上面几点提示,可能关键时候会直接影响到程序性能。分别看看这几句话在源码上如何实现的。

1.当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项;

先看下全局混入如何实现的:

function initGlobalAPI(Vue){
    //...此前代码省略
    initMixin$1(Vue);
    //...此后代码省略
}
复制代码

initGlobalAPI是全局函数,也是全局API的入口,其中就包含了全局混入。

function initMixin$1(Vue) {
    //全局混入
    Vue.mixin = function (mixin) {
      this.options = mergeOptions(this.options, mixin);
      return this;
    };
}
复制代码

由此可见核心方法在于mergeOptions,那mergeOptions干了些什么:

  /**
  这里主要干了两件事:
  1、检查和序列化选项;
  2、合并选项
  */
 function mergeOptions(parent, child, vm) {
    //检查是否是合法的组件
    {
      checkComponents(child);
    }

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

    normalizeProps(child, vm);
    normalizeInject(child, vm);
    normalizeDirectives(child);
    
    //这里的_base是Vue对象
    if (!child._base) {
      if (child.extends) {
        parent = mergeOptions(parent, child.extends, vm);
      }
      //mixins是数组,遍历多个混入
      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;
    //优先遍历父组件选项
    for (key in parent) {
      mergeField(key);
    }
    //再遍历子组件选项
    for (key in child) {
      if (!hasOwn(parent, key)) {
        mergeField(key);
      }
    }
    //注意这里的strats对象
    function mergeField(key) {
      var strat = strats[key] || defaultStrat;//合并策略
      options[key] = strat(parent[key], child[key], vm, key);
    }
    return options;
  }
复制代码

上面代码看起来挺简单,不就是合并对象吗,我们看下上面要注意的strats对象是什么就知道了。

var strats = config.optionMergeStrategies;
复制代码

strats是一个全局变量,用来重写父选项和子选项的合并规则。,它的属性包括组件的所有配置项。

2.数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先,同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用

3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对;

//返回合并后的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);
  };

复制代码
function mergeDataOrFn(parentVal, childVal, vm) {
    if (!vm) {
      // Vue.extend合并,都必须是函数
      if (!childVal) {
        return parentVal;
      }
      if (!parentVal) {
        return childVal;
      }
      
      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;
        }
      };
    }
  }
复制代码
//这里就干了一件事,递归合并data
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;
  }
复制代码
//合并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;
  };
复制代码
//合并生命周期钩子函数
 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;
  }
复制代码
//合并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];
      }
      ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child];
    }
    return ret;
  };
复制代码

看到这我们应该明白了上面所说的第2、3点,钩子函数实际是存储在一个数组队列中,这里是父组件钩子函数concat子组件钩子函数,所以同名钩子函数都会执行,先执行父组件钩子函数,再执行子组件钩子函数。

所以在实际业务中用到全局mixin时候,我们会经常发现有些方法被执行了多次,先后顺序是不一样的。因此也就不难理解前文提到的第4点,谨慎使用全局mixin,因为它会在所有的组件中都会执行,一旦组件非常多就会导致严重的性能问题。举个例子,通常我们会把一个页面分成若干组件,如果一个列表有100个item组件,那么全局混入中的方法至少执行100次以上,如果其中有比较耗性能的方法,那么就会扩大100倍以上。所以当遇到有严重性能问题的时候,不妨看看是不是全局混入导致的。

小结一下

1、Vue混入实际上是各种属性和方法的合并,只是合并策略不一样;
2、混入的时候data,props,computed,methods,inject的合并规则是子组件会拓展或者覆盖父组件的值,优先子组件;
3、钩子函数及watch的合并是数组的合并,所以父组件、子组件都会执行,父组件先执行,所以慎用全局混入,可能会导致严重的性能问题。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享