深入 vue mixins

1,326 阅读6分钟

vue混入方案mixin,借助该方法我们可以实现组件的逻辑复用,精简代码。一个mixin对象包含vue实例的所有组件选项,并且各个选项将按照一定的规则进行合并。

如果对该方法不甚了解的伙伴,可以先移步 vuex官方文档-混入 进行学习。本文仅只作为官方的文档的扩展,意图对mixins这个概念做一次深入理解。闲话不多说,我们先看全局混入方法mixin

全局混入 Vue.mixin

// core/global-api/mixin.js

import { mergeOptions } from '../util/index'

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

无需惊讶,代码的确如此简短。内部仅作mergeOptions的一次调用。this.options指的是全局挂载在构造函数Vue上的选项,包括mixinscomponentsfiltersdirectives等等。这些全局选项将在创建vue实例时并入组件选项中,在并入时,其实依然是调用的mergeOptions方法。

//core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this

    ...

    // 该方法将合并选项的同时,规则化组件选项
    vm.$options = mergeOptions(
      // 此处vm为Vue的实例,其constructor指向Vue
      // 该方法,取出Vue.options上的选项值
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )

    ...
  }
}

进入重点,mergeOptions

选项合并 mergeOptions

// core/util/options
// ... 省略部分代码
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  
  ...
  // 优先递归合并extends
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  // 再递归合并mixins
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }

  ...
  
  // 最终的options值
  const options = {}
  let key
  // 首先将parent中的选项通过strat存储的key对应合并规则,加入到options中
  // 注意:mergeField方法将合并parent[key]、child[key]
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    // 此处避免child[key]重复合并
    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
}

合并规则 strats

上文反复强调合并规则,那么strats挂载的规则究竟是哪些呢?相信稍微了解mixins的同学都知道混入中,同名钩子函数如created将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。datamethods 此类值为对象的选项,键值发生冲突时,以组件优先。

defaultStrat

mergeField方法中会看到一个默认的strat值:defaultStrat。该方法优先取值childVal,若不存在,则取parentVal。这正是我们通常所说的组件优先的原则。

// core/util/options
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

data

data选项采用组件优先的合并原则。但是在此同时,对于引用类型的数据,进行了深拷贝的优先策略。

// core/util/options

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    ...
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

mergeDataOrFn其实仅仅做判断数据格式是否满足的逻辑处理,这里就省略不占篇幅了,该方法最终将调用一个名叫mergeData的方法,最终的合并原则都在其中:

// core/util/options
// 注意to为组件属性值,from为混入或者extend的属性值
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]
    // 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
}

to为组件本身的对象属性,from为其他混入或者继承的数据。因此data混入时遵守组件优先,对象属性递归合并。

同时,provide也遵守该规则

strats.provide = mergeDataOrFn

Hooks

除了生命周期常见的生命周期函数,还可以看见额外的两个组件方法errorCaptured错误捕获事件和serverPrefetch vue ssr的服务器数据预拉取方法。

// shared/constants.js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
// core/util/options
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

mergeHook是主要的合并策略方法。可以看到里面套了三层的三元运算。我们改成if-else便于阅读:

let res = []
// childVal存在
if (childVal) {
  // parentVal存在,合并childVal
  if (parentVal) {
    res = parentVal.concat(childVal)
  } else {
    // 保证childVal为数组
    res = Array.isArray(childVal) ? childVal : [childVal]
  }
} else {
  res = parentVal
}

if-else实在有点多,如此我们简化理解为:

const res = [].concat(parentVal || [], childVal || [])

由此生成对应hook的执行队列,先入先出。

Assets

assets包含熟悉的三个属性component、directive、filter

// shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

声明strats时自动加了s后缀:

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

依然遵循组件优先策略,只是代码内部进行了一些类型判断:

// core/util/options
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

watch

watch略显特殊,省略了部分if-else代码,集中看重点规则定义逻辑:

// core/util/options
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  ...
  const ret = {}
  // parentVal浅拷贝赋值给ret
  extend(ret, parentVal)
  // 迭代childVal
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    // 规范parent为数组结构
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    // 三元运算,我们根据hook策略中的经验简化一下:
    // ret[key] = [].concat(parent || [], child)
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

可以看出,watch中相同的属性值被整合成了数组,当监听发生改变,依次调用。

Other object hashes

其他对象属性包括:propsmethodsinjectcomputed遵守相同的规则:

// core/util/options
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  ...
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  // childVal存在将覆盖parentVal相同的属性值
  if (childVal) extend(ret, childVal)
  return ret
}

不难看出,依然是组件属性优先的策略。

自定义选项合并策略

vue提供了一个自定义选项合并策略的属性optionMergeStrategies,该属性将被默认为strats的初始值:

// config即为Vue.config
const strats = config.optionMergeStrategies

自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies 添加一个函数

Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // 返回合并后的值
}

总结

  1. dataprovide合并时以组件优先,对于对象类型的数据将递归合并
  2. 生命周期合并时,将同名hook整合成数组,组件的生命周期最后调用
  3. watch中同名键值将整合成数组,组件中的watch同样最后调用
  4. 其他包括assets type(components、filters、directives)propsmethodsinjectcomputed则同名覆盖,组件优先
  5. 如果有自定义选项需要使用合并策略,可使用optionMergeStrategies自定义合并策略

以上。