【源码解析】揭开mixin的神秘面纱,原来就是对象的合并

173 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

前言

大家好,今天我们继续来学习Vue2中的另一个全局API - mixin。不知道大家在平时开发中有没有用过mixin,用过的小伙伴应该都知道:mixin的主要用途就是用来做混入用的,而混入又分为全局混入和局部混入两种。其实简单用一句话来概括就是:(以全局混入为例)

将mixin对象中的属性与各个组件中的属性进行合并

为什么这么说呢,等我们看完后面的源码就明白了。下面我们先来用一个简单的小案例来展示一下mixin的用法及作用。

案例演示

假设有这么一个场景:在一个项目中有a.vue、b.vue和c.vue三个组件,现要求程序启动后在每个组件中都显示一句话“欢迎来到Vue世界”,同时还需要将这句话通过一个方法输出到控制台中。如果用传统的方法来实现就是:给每个组件都添加一个msg属性,并将msg通过vue的小胡子({{msg}})语法展示在页面上,然后再定义一个pringMsg方法将msg输出到控制台中。从效果上来说确实能够实现我们想要的功能。但是这样明显有个问题就是同样的代码我们要写三遍,而且也会造成代码的冗余。这个时候如果使用mixin来做个全局混入,不但能够实现相同的效果还能减少代码的冗余。下面来看下代码和效果图

  • main.js 全局混入
Vue.mixin({
    data(){
        return {
            msg:'欢迎来到Vue世界'
        }
    },
    methods:{
        printMsg(){
            console.log(this.msg)
        }
    }
})
new Vue(...)
  • App.vue 注册并使用a/b/c组件
<div id="app">
    组件a:<mya />
    组件b:<myb />
    组件c:<myc />
</div>
import mya from './a.vue'
import myb from './b.vue'
import myc from './c.vue'
export default{
    components:{mya,myb,myc}
}
  • a.vue/b.vue/c.vue 三个组件代码相同
<div>{{msg}}</div>
created(){
    this.printMsg();
    console.dir(this)
}

caolouyaqian.gif

从上图我们看到,a/b/c三个组件中都展示出了“欢迎来到Vue世界”这条数据,并且也都在控制台中做了输出。当我们把每个组件的实例输出后发现:每个组件中都多出了个msg响应式属性和printMsg方法,这也说明了mixin中msg和printMsg被合并到了每个组件中了。另外还有一个特点:

如果子组件与mixin中都有个相同名字的msg属性,那么子组件的msg会覆盖掉mixin中的msg。也就是说以子组件的属性优先。其它methods或props也是同样以子组件优先。

源码解析

通过上面的案例了解了mixin的用法及用途。接下来我们再来看下它是如何做到让所有组件都能够拥有相同的属性或方法的,我们为什么又说它就是对象属性的合并?下面我们来解析下mixin的源码:

Vue.mixin = function (mixin: Object) {
  this.options = mergeOptions(this.options, mixin)
  return this
}

看了源码你会不会觉的很惊讶:竟然只有两句代码?其实远没有这么简单,只不过是对核心代码进行了封装。下面我们先来简单看下这个mixin函数:

  • 首先mixin是Vue类的一个静态方法,该方法接收一个对象类型的mixin参数
  • 在函数体内调用了一个mergeOptions的方法,并将this.options和mixin作为参数传给mergeOptions,同时将返回值重新赋值给this.options
  • 最后再将this返回。 这里只需要注意的是mixin是Vue的静态方法,每次都是通过Vue.mixin来调用,那么这个函数里面的this其实就是Vue本身。这里主要做的事情就是将mixin中的数据与Vue的options中的进行合并后再重新赋值给Vue的options,而实现合并的核心主要还是在于这个mergeOptions方法。接下来我们重点分析下mergeOptions的实现(mixin的核心)。
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

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

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }

  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
  • 首先mergeOptions接收3个参数分别是对象类型的parent、child和Vue的实例vm
  • 第一句在非生产环境下检查child(mixin)中提供的组件名称是否符合规范
  • 接下来判断如果传进来的child是一个function类型,则让child的options重新赋值给child
  • 下面的三个normalizeXXX则是对props、inject和diretive做一些标准化处理,因为这些东西在使用时既可以是数组也可以是对象,这里会进行标准化统一改造成对象的形式,以便后面处理
  • 继续往下则是检测如果child中还有extends或者mixins,则将这些内容也通过mergeOptions合并到parent上。这里还有一点就是只有child上没有_base属性时才会进行extend和mixin的合并操作。从注释来看只有合并过的options才会有_base属性,因此也就是说如果child上已经有了_base就说明已经合并过了,下次就不需要再重复合并了。
  • 遍历parent上的所有属性并调用mergeField做合并处理
  • 遍历child上所有属性并调用mergeField做合并处理,这里要注意的是如果对应的属性在parent上已经存在,就不做合并处理了,也正是因为这一步才使得mixin中的属性不会覆盖组件本身原有的属性(这里的parent就相当于我们上面案例中的a/b/c组件,而child则是mixin中的对象)
  • 最后一步就是mergeField函数,该函数是实现对象合并的关键,我们需要详细解读一下
    • 首先通过strats和key得到一个策略函数strat
    • 然后执行这个策略函数strat实现parent[key]和child[key]的合并
    • 以上是我们通过代码就能直观的分析得到,而实际上strats是一个对象,该对象中保存了一些以methods、props、inject等命名的策略函数,而参数key则是methods、props等,因此根据strats[key]就能得到对应的策略函数,最后再执行这些策略函数实现真正的合并。

下面代码是以methods为例,实现了methods中的方法合并的流程(节选自src/core/util/options.js 242行) parentVal和childVal分别是外面传进来的组件和mixin的methods对象,然后通过extend方法将两个对象中的方法合并到ret对象上,最后将合并后的ret返回。在extend中就是遍历methods对象并将每一属性添加到ret上

strats.methods = function(parentVal,childVal,vm,key){
    const ret = Object.create(null);
    extend(ret, parentVal)
    if(childVal) extend(ret, childVal)
    return ret
}

总结

本次分享我们先是通过一个简单的案例展示了mixin的用法和作用,最后又通过对源码的分析彻底掌握了mixin实现混入的原理。最后总结一句话就是:

将组件中不存在但在mixin对象中存在的每一个属性合并到各个组件中去

好了今天的分享就到这里了。