Vue原理:Vue.mixin合并策略

1,864 阅读3分钟

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

Vue.mixin是一个使用恰当时会是一个非常优秀的API,滥用或者使用不当时,将会是一场噩梦,这也是官方不推荐在应用项目中使用的原因,虽然其存在一些弊端:命名冲突、来源混乱等,但是不妨碍我们对其原理进行探索的步伐

先看一下其如何使用,现在有这么一段代码

import Vue from "vue";

Vue.mixin({
  created: function () {
    console.log("mixins created");
  },
});

new Vue({
  created() {
    console.log("实例 created");
  },
});

mixins会将内部的created和组件的created进行合并,控制台会先后输出mixins created实例 created

Vue.mixin

上文调用initGlobalAPI函数进行全局API初始化时,内部调用了initMixin(Vue)mixin进行全局API注册,代码如下:省略无关代码

export function initGlobalAPI(Vue) {
  // 整合所有的全局相关的内容
  Vue.options = Object.create(null);
  initMixin(Vue);
}

注册全局mixin代码如下:其内部做的事情并不多,合并选项的动作全部抽取到了mergeOptions函数中

export function initMixin (Vue) {
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

export function mergeOptions(parent, child){
  // todo...
}

在使用mixin过程中,本质是对于parentchild选项的合并,其实就是两个对象合并的一个过程

遍历parentchild

// 遍历 parent
for (const key in parent) {
  mergeField(key);
}

// 遍历 child
for (const key in child) {
  // 如果已经合并过了, 就不需要再次合并了
  if (!parent.hasOwnProperty(key)) {
    mergeField(key);
  }
}

具体的合并策略封装在mergeField函数中,在编写其逻辑之前先回顾一下mixin对于同名选项时是以怎样的方式进行合并

  1. 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。比如生命周期
  2. 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

针对不同的情况mixin会采用不同的合并手段处理,首先看一看对于值为对象的情况是如何合并,也就是默认的合并策略

function mergeField(key) {
  // 都是对象
  if (typeof parent[key] === "object" && typeof child[key] === "object") {
    // 键名冲突,以child的覆盖parent,即取组件的
    options[key] = {
      ...parent[key],
      ...child[key],
    };
  } else if (child[key] === null) {
    // 以 parent 为准
    options[key] = parent[key];
  } else {
    // 以 child 为准
    options[key] = child[key];
  }
}

对于同名钩子合并,比如生命周期的合并,需要特殊处理

首先将全部的生命周期定义

const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
];

生命周期钩子的合并是一样的,因此可以遍历LIFECYCLE_HOOKS定义每个钩子的行为

function mergeHook(parentVal, childVal) {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal) // parent child 都有, 需要变成数组
      : Array.isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal; // 有child  直接使用老的
    
  return res ? dedupeHooks(res) : res;
}

/** 
 * 删除重复的数据
*/
function dedupeHooks(hooks) {
  const res = [];
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i]);
    }
  }
  
  return res;
}

// 遍历全部的生命周期钩子
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

Vue.mixin中,对于componentsdirectivesfilters等选项都有进行特殊的处理