Vue混入Mixin——使用方法、注意事项及源码分析

使用vue开发过程中,常用的抽离公共逻辑代码的方式有两种,一种是不依赖Vue实例的纯数据处理代码,可以写成utils工具方法;一种是依赖Vue实例的逻辑代码,则使用混入(Mixin)。混入虽然好用,但是随着项目代码增多,其管理和维护的成本极速增大,牵一发而动全身,Vue3.0 Composition API 便是试图解决这类问题,感兴趣的同学可以阅读浅尝vue3.0,万字总结初步了解。

本文使用Vue版本2.6.2进行分析,例子来自于Vue官方教程

混入的使用方法

Mixin的使用方法非常简单,它可以混入包含任意组件选项的对象至目标组件中。在未定义“混入”规则的情况下,Vue本身会采取默认的规则将这些选项“合并”至目标组件中。

举个例子:

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

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

var component = new Component() // => "hello mixin"

当目标组件和待混入的对象含有同名组件选项时,vue默认的“合并规则”如下:

data选项合并

data对象的内部会进行递归合并,当发生冲突时优先使用目标组件的数据。

例子

var mixin = {
    
    
  data: function () {
    
    
    return {
    
    
      message: 'hello',
      foo: 'abc'
    }
  }
}

new Vue({
    
    
  mixins: [mixin],
  data: function () {
    
    
    return {
    
    
      message: 'goodbye',
      bar: 'def'
    }
  },
  created: function () {
    
    
    console.log(this.$data)
    // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

源码分析

从源码中我们可以看到,数据对象会递归的执行合并操作,如果出现相同则不进行合并,即使用目标组件自身的数据

function mergeData (to: Object, from: ?Object): Object {
    
    
  if (!from) return to
  let key, toVal, fromVal

  // 获取对象的键名
  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    
    
    key = keys[i]
    // 具有可观察对象实例,直接跳过,不需要更换
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    // 属性名相同时不进行赋值操作,即使用目标组件的值
    if (!hasOwn(to, key)) {
    
    
      // 新增属性,使用set方法将其变为响应式
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
    
    
      // 对象递归执行
      mergeData(toVal, fromVal)
    }
  }
  return to
}

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
    
    
  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
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
    
    
        return mergeData(instanceData, defaultData)
      } else {
    
    
        return defaultData
      }
    }
  }
}
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
    
    
  if (!vm) {
    
    
    if (childVal && typeof childVal !== 'function') {
    
    
      process.env.NODE_ENV !== 'production' && 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)
}

生命周期钩子函数

同名的钩子函数会合并成一个数组,混入对象的钩子函数先执行。

例子

var mixin = {
  created: function () {
    console.log('混入对象的钩子被调用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('组件钩子被调用')
  }
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

源码分析

  1. 目标组件没有这个钩子函数,直接使用混入的钩子函数
  2. 目标组件有钩子函数:
    • 如果混入对象也该钩子函数,则混入对象的钩子函数拼接目标组件的钩子函数为一个数组
    • 如果混入对象没有钩子函数,返回目标组件的狗子函数组成的数组

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
    
    
  /*
    1. 目标组件没有这个钩子函数,直接使用混入的钩子函数
    2. 目标组件有钩子函数
      2.1 如果混入对象也该钩子函数,则混入对象的钩子函数拼接目标组件的钩子函数为一个数组
      2.2 如果混入对象没有钩子函数,返回目标组件的狗子函数组成的数组
  */
  const res = childVal
    ? parentVal
      // 混入对象的钩子函数数组拼接上
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  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
})

值为对象的选项

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

源码分析

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
    
    
  if (childVal && process.env.NODE_ENV !== 'production') {
    
    
    assertObjectType(key, childVal, vm)
  }
  // 混入对象没有,直接使用目标组件的数据
  if (!parentVal) return childVal
  const ret = Object.create(null)
  // 先混入“混入对象”的数据
  extend(ret, parentVal)
  // 将目标组件的对象混入至对象中,相同属性覆盖。最终使用目标组件数据
  if (childVal) extend(ret, childVal)
  return ret
}
strats.provide = mergeDataOrFn

// shared/utils.js
// 将原对象属性混入到目标对象中
export function extend (to: Object, _from: ?Object): Object {
    
    
  for (const key in _from) {
    
    
    to[key] = _from[key]
  }
  return to
}

猜你喜欢

转载自blog.csdn.net/sinat_36521655/article/details/109187797