Vue mixin 的理解和使用

265 阅读2分钟

一、基本概念

  • 官方定义:mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能

  • mixin 本质上是一个 JavaScript 对象,保护组件的所有功能选项,比如 data 、components、methods、created、computed 等等;只要将共用的功能以对象的方式传入 mixins 选项中,当组件使用时 mixins 对象时,所有 mixins 对象的选项都将被混入该组件本身的选项中来

// 局部混入
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}
// 组件通过 mixins 属性调用 mixin 对象,混入 mixin 里的方法
// 组件使用的时候,回执行 mixin 中的生命钩子函数

Vue.component('componentA',{
  mixins: [myMixin]
})


// 全局混入
Vue.mixin({
  created: function () {
      console.log("全局混入")
    }
})
// 全局混入会影响到所有组件实例
// 全局混入常用于插件的编写

二、使用场景

开发过程中,不同的组件中经常会需要用到一些相同或者相似的代码,并且这些代码的功能相对独立,就可以通过 mixin 将相同的代码抽离出来

案例

// 一个 modal 弹窗组件,内部通过 isShowing 来控制显示
const Modal = {
  template: '#modal',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}
// 一个 tooltip 提示框组件,内部通过 isShowing 来控制显示
const Tooltip = {
  template: '#tooltip',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}
// 上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同,就可以抽离出共同代码
const toggle = {
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}
// modal 弹窗组件
const Modal = {
  template: '#modal',
  mixins: [toggle]
};
// tooltip 提示框组件
const Tooltip = {
  template: '#tooltip',
  mixins: [toggle]
}

三、源码分析

// src/core/global-api/mixin.js 
export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 主要调用 merOptions 方法,合并选项
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

// src/core/util/options.js
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {

  // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
  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
  // 先遍历 parent 的key 调对应的 strats[XXX] 方法进行合并
  for (key in parent) {
    mergeField(key) 
  }
  for (key in child) {
    if (!hasOwn(parent, key)) { // 如果 parent 已经处理过某个key 就不处理了
      mergeField(key) // 处理 child 中的 key,也就 parent 中没有处理过的key
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    // 根据不同类型的options调用strats中不同的方法进行合并
    options[key] = strat(parent[key], child[key], vm, key) 
  }
  return options
}
  1. 优先递归处理 mixins
  2. 先遍历合并 parent 中的 key ,调用 mergeField 方法进行合并,然后保存在变量 options
  3. 再遍历 child ,合并补上 parent 中没有的 key ,调用 mergeField 方法进行合并,保存在变量 options
  4. 通过 mergeField 函数进行了合并

Vue 中的的合并策略:

  • 替换型: propsmethodsinjectcomputed
  • 同名的propsmethodsinjectcomputed会被后来者代替
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
  const ret = Object.create(null) // 创建一个第三方对象 ret
  extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
  if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
  return ret
}
strats.provide = mergeDataOrFn
  • 合并型 data
  • mergeData 函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:
  • 当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set 方法其实就是一些合并重新赋值的方法)
  • 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性
strats.data = function(parentVal, childVal, vm) {    
    return mergeDataOrFn(
        parentVal, childVal, vm
    )
};

function mergeDataOrFn(parentVal, childVal, vm) {    
    return function mergedInstanceDataFn() {        
        var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
        var parentData = parentVal.call(vm, vm)        
        if (childData) {            
            return mergeData(childData, parentData) // 将2个对象进行合并                                 
        } else {            
            return parentData // 如果没有childData 直接返回parentData
        }
    }
}

function mergeData(to, from) {    
    if (!from) return to    
    var key, toVal, fromVal;    
    var keys = Object.keys(from);   
    for (var i = 0; i < keys.length; i++) {
        key = keys[i];
        toVal = to[key];
        fromVal = from[key];    
        // 如果不存在这个属性,就重新设置
        if (!to.hasOwnProperty(key)) {
            set(to, key, fromVal);
        }      
        // 存在相同属性,合并对象
        else if (typeof toVal =="object" && typeof fromVal =="object") {
            mergeData(toVal, fromVal);
        }
    }    
    return to
}
  • 队列型:全部生命周期和watch
  • 生命周期钩子和watch被放到一个队列中,然后正序遍历一次执行,mixin 中的生命周期钩子是比组件对应的生命周期勾子先执行
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

// watch
strats.watch = function (
  parentVal,
  childVal,
  vm,
  key
) {
  if (parentVal === nativeWatch) { parentVal = undefined; }
  if (childVal === nativeWatch) { childVal = undefined; }
  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
};
  • 叠加型:componentdirectivesfilters
  • 叠加型主要是通过原型链进行层层的叠加
strats.components=
strats.directives=

strats.filters = function mergeAssets(
    parentVal, childVal, vm, key
) {    
    var res = Object.create(parentVal || null);    
    if (childVal) { 
        for (var key in childVal) {
            res[key] = childVal[key];
        }   
    } 
    return res
}