合并配置(options)

548 阅读2分钟

在初始化 Vue 实例的过程中,有一段逻辑涉及到 options 的合并,这其中分为两种场景:首次初始化时 options 合并和组件 options 合并。它们合并的逻辑有所不同,这也是本文所要探究的内容。

合并配置(options)

按照惯例,沿着主线将其整理成一张逻辑图,如下:

options.png

回顾一下,在初始化 Vue 实例时,合并 options 配置逻辑如下:

// merge options
if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

从代码逻辑可以看出,合并配置分两种情况:首次初始化时配置合并和组件化配置合并。下面将一一来分析它们是如何实现合并的?

首次初始化配置合并

在首次初始化 Vue 实例时,外部传入 options 不包含属性 _isComponent ,于是执行 else 逻辑,即调用 mergeOptions 函数来实现 options 配置合并。在分析 mergeOptions 之前,先来看下 resolveConstructorOptions 是如何实现的?

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

函数接收一个参数:Ctor,数据类型为 Class<Component> ,这里传入的参数:vm.constructor ,即为 Vue 构造函数,结果返回的是 Vue.options。而 Vue.options 是在 initGlobalAPI 函数定义的,位于 src/core/global-api/index.js,相关的逻辑实现如下:

Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

extend(Vue.options.components, builtInComponents)

首先通过 Object.create(null) 创建空对象,赋值给 Vue.options ;接着看下 ASSET_TYPES 具体指的是什么?

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

ASSET_TYPES 是一个数组,包含 componentdirectivefilter ,遍历该数组,分别设置为 Vue.options 属性,其值通过 Object.create 创建的空对象。除此之外,还设置属性 _base,指向构造函数 Vue 以及通过 extend 设置内置组件 KeepAlive

回到 src/platforms/web/runtime/index.js,有这么一段逻辑:

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

分别在 Vue.options 属性 directivescomponents 设置平台指令:showmodel 以及平台组件 TransitionTransitionGroup ,至此,Vue.options 具有的属性如下:

Vue.options = {
  component: {
    KeepAlive,
    Transition,
    TransitionGroup
  },
  
  directives: {
    show,
    model
  },
  
  filters: {},
  _base: Vue
}

resolveConstructorOptions 分析完,那么接下来分析 mergeOptions 的具体实现:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
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)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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
}

函数接收三个参数,分别如下:

  • parent:数据类型为 Object,在这里指的是 Vue.opitons
  • child:数据类型为 Object,在这里指的是 new Vue 传入的对象;
  • vm :数据类型为 Component,表示 Vue 实例。
if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}

开发环境下对 child 进行校验,即检查 child 是否包含属性 components,如果包含的话,则检查组件名命名是否合法,具体实现如下:

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

export function validateComponentName (name: string) {
  if (!new RegExp(`^[a-zA-Z][\-\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

这三行代码的主要作用是:如果 child 包含属性 propsinjectdirectives,则对它们进行规范化处理,比如名称、传入的类型等等。

// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
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)
    }
  }
}

这段代码的主要作用是判断 child 是否包含属性 extendsmixins,如果包含的话则调用 mergeOptions 对它们进行处理。

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

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

这里才是对 options 进行合并的逻辑,先遍历 parent ,即 Vue.options 属性,调用函数 mergeField 进行合并;然后再遍历 child ,即外部传入的 opitons 属性,判断 parent 是否属性。如果不包含的话,则通用调用 mergeFiled 进行属性合并。那么 mergeField 又是如何实现的呢?

function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}

首先来看下 starts 具体指的是什么?其定义位于 src/core/unit/options.js,相关代码如下:

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const strats = config.optionMergeStrategies

/**
 * Options with restrictions
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

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)
}

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

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

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */
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

从代码可以看出,starts 默认是一个空对象,然后设置各种属性,这里需要知道两个常量具体指的是什么?分别是:LIFECYCLE_HOOKSASSET_TYPES ,如下:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

那么,starts 最终包含的属性如下:

starts = {
  el: () => {},    // 开发环境
  propsData: () => {},    // 开发环境
  data: () => {},
  beforeCreate: mergeHook,
  created: mergeHook,
  beforeMount: mergeHook,
  mounted: mergeHook,
  beforeUpdate: mergeHook,
  updated: mergeHook,
  beforeDestroy: mergeHook,
  destroyed: mergeHook,
  activated: mergeHook,
  deactivated: mergeHook,
  errorCaptured: mergeHook,
  serverPrefetch: mergeHook,
  component: mergeAssets,
  directive: mergeAssets,
  filter: mergeAssets,
  watch: () => {},
  props: () => {},
  methods: () => {},
  inject: () => {},
  computed: () => {},
  provide: mergeDataOrFn
}

回到 mergeField,其核心逻辑是通过传入 key,从对象 starts 获取其值,如果存在的话,则执行相对应的函数;否则执行默认的函数,即 defaultStrat,具体实现如下:

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

重新梳理下合并逻辑,根据不同的 key 采用不同的策略对其进行合并,最后返回合并后的 opiotns

组件化配置合并

Vue 内部在定义子组件时,设置属性 _isComponent 表示其是一个组件,那么在合并配置时则会调用函数 initInternalComponent ,具体实现如下:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

函数接收两个参数:

  • vm :数据类型为 Component,表示 Vue 实例;
  • options:数据类型为 InternalComponentOptions),表示传入的 opitons

先获取 Vue 构造函数 options,即 Vue.options,通过 Object.create 创建一个对象赋值 optsvm.$options,也就是说,Vue.options 作为属性 optsvm.$options 的原型;然后设置相关的属性,最终完成配置合并。相比首次初始化配置合并,其实现相对比较简单。

至此,合并配置的逻辑已经分析完了。

参考链接