合并配置

我们创建组件实例时,都会调用vm._init去初始化组件。而初始化组件的首要任务就是将跟组件实例有关的所有配置合并并返回新的配置对象,比如通过Vue.extend继承而来的配置、通过Vue.mixins或组件选项对象里mixin选项混合而来的配置等等。

根组件 VS 子组件

我们知道,创建组件实例时一般有两种情况:用户通过new Vue(options)显示创建、渲染组件时创建子组件,而这两种情况合并配置的方式也是不一样的。

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // ...
  // merge options
  if (options && options._isComponent) {
    // 子组件合并配置
    initInternalComponent(vm, options)
  } else {
    // 根组件合并配置
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

根组件

对于根组件来说,会将(经过处理的)vm.constructor.options和调用new Vue(options)传入的options合并。

这里我们需要弄明白:第一,vm.constructor.options是怎么来的以及有哪些内容;第二,resolveConstructorOptions是做什么用的。

Vue.options

我们先忽略继承的情况,先讨论根组件实例的vm.constructor为构造函数Vue的情况。在initGlobalAPI(Vue)初始化全局 API 的时候,以及在导出Vue的时候,已经对Vue.options进行了初始化和处理。

// src/core/global-api/index.js
import builtInComponents from '../components/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  // 初始化 components、directives、filters
  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)
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/platforms/web/runtime/index.js
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
export default Vue
1
2
3
4
5
6
7
// src/shared/constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
1
2
3
4
5
6
// src/core/components/index.js
import KeepAlive from './keep-alive'

export default {
  KeepAlive
}
1
2
3
4
5
6

最终,Vue.options的数据结构大概是如下这样。

Vue.options = {
  components: {
    KeepAlive,
    Transition,
    TransitionGroup
  },
  directives: {
    model,
    show
  },
  filters: {},
  _base: Vue
}
1
2
3
4
5
6
7
8
9
10
11
12
13

resolveConstructorOptions

更新:全局方法Vue.component/directive/filter也会导致Vue.options增加新的内容。

若是想详细了解本节的内容,请先了解Vue.extend的实现。

resolveConstructorOptions的主要作用是,若是vm.constructor是继承而来的构造函数,需要重新对vm.constructor.options进行重新合并,以加入调用vm.constructor.mixin()vm.constructor.options的修改以及调用vm.constructor.super.mixinvm.constructor.options.super的修改。

主要注意的是,为了保证合并的顺序,vm.constructor.super.options需要重新计算,vm.constructor.extendOptions也要重新更新,最后再mergeOptions(vm.constructor.super.options, vm.constructor.extendOptions)来得到vm.constructor.options

/**
 * 返回最新的 Ctor.options,以及更新 Ctor.extendOptions
 *
 * 1. 若 Ctor 不是通过 Vue.extend 继承而来的,直接返回 Ctor.options
 * 2. 否则,返回计算而来的最新的 Ctor.options。此处要考虑的问题是
 *    a. 继承的基类 Ctor.super.options 可能发生变化(通过调用 Ctor.super.mixin() 而造成的)
 *    b. Ctor.options 可能发生变化(通过调用 Ctor.mixin() 而造成的)
 */
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 如果存在父类,即该 Ctor 是继承而来的子类
  if (Ctor.super) {
    // 当前计算得的最新的 Ctor.super.options
    const superOptions = resolveConstructorOptions(Ctor.super)
    // 子类继承时保存的 Ctor.super.options
    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) {
        // 动态更新 Ctor.extendOptions,以确保其包含了通过 Ctor.mixin 添加、修改的选项(配置合并不会出现删除的情况)
        extend(Ctor.extendOptions, modifiedOptions)
      }

      // 基于最新的 Ctor.super.options 和 Ctor.extendOptions 合并配置
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}


/**
 * 返回通过调用 Ctor.mixin 从而导致 Ctor.options 里选项改变的那些选项及其值
 */
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified

  // 最新的 Ctor.options(可能已经通过 Ctor.mixin 改变了)
  const latest = Ctor.options

  // 上一次继承 Super 时调用 Super.extend(extendOptions) 传入的选项对象
  const extended = Ctor.extendOptions

  // 上一次继承 Super 时(合并)Ctor.options 后的副本
  const sealed = Ctor.sealedOptions

  for (const key in latest) {
    // Ctor.mixin 是通过 mergeOptions 合并选项的,返回的 value 都是新的引用对象
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      // 返回 Ctor.options 里改变的选项值(经过去重)
      modified[key] = dedupe(latest[key], extended[key], sealed[key])
    }
  }
  return modified
}

/**
 * 数据去重,若选项值不是数据,直接返回 latest,否则返回那些在 latest 里 &&(在 extended 里 || 不在 sealed 里的)
 */
function dedupe (latest, extended, sealed) {
  // compare latest and sealed to ensure lifecycle hooks won't be duplicated
  // between merges
  if (Array.isArray(latest)) {
    const res = []
    sealed = Array.isArray(sealed) ? sealed : [sealed]
    extended = Array.isArray(extended) ? extended : [extended]
    for (let i = 0; i < latest.length; i++) {
      // push original options and not sealed options to exclude duplicated options
      // 筛选出:曾经在 extended 里以及 后来通过 Ctor.mixin 加入的(即不在 sealed 里)
      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
        res.push(latest[i])
      }
    }
    return res
  } else {
    return latest
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

mergeOptions

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 检查 option 里的 components 的 name 是否符合要求
    checkComponents(child)
  }

  if (typeof child === 'function') {
    // child 是构造函数
    child = child.options
  }

  // 标准化 props、inject、directives 为对象格式
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, 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) {
    // 先合并 parent 里有的选项
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      // 再合并 child 里有但 parent 里没有的选项,避免重复合并
      mergeField(key)
    }
  }
  function mergeField (key) {
    // 优先使用选项单独的合并策略,没有的话使用默认策略
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

mergeOptions主要的功能就是,将parentchild配置进行合并,包括要合并child配置的extendsmixins。在合并具体选项时,我们可以看到不同的选项合并策略函数strat可能是不一样的,如果不存在已知的合并策略函数,则将使用默认的合并策略函数。需要注意的是,开发者还可以提供自定义的合并策略函数。具体的合并策略,我们将在不同选项的合并策略详细描述。

子组件

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

子组件在调用的vm._init时传入的options._isComponenttrue,因此是通过initInternalComponent(vm, options)来合并配置的。

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // 子组件的占位 VNode
  const parentVnode = options._parentVnode
  // 创建子组件时的活动实例
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  // 将组件占位 VNode 上有关组件的数据,转存到 vm.$options 上
  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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

initInternalComponent主要做的是,基于vm.constructor.options创建vm.$options,并将如下数据转存到vm.$options,方便后续使用。

  • 创建组件占位 VNode 时存储在vnode.componentOptions里的数据比如propsDatalistenerschildrentag
  • 创建子组件时的选项options上的parentrenderstaticRenderFns等数据
export function createComponent (
  // ...
) {
  // ...
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    // vnode.componentOptions
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

不同选项的合并策略

组件选项对象里存在着各种选项,而各个选项的合并策略可能是不同的,需要在合并时通过选项的key获取到对应的选项策略函数,这些选项策略函数都预置在config.optionMergeStrategies对象里,而config.optionMergeStrategies对象初始时是空对象,后续会针对不同选项添加对应的选项策略函数。

// src/core/util/options.js
import config from '../config'
// ...
const strats = config.optionMergeStrategies
export function mergeOptions (
  // ...
): Object {
  // ...
  function mergeField (key) {
    // 优先使用选项单独的合并策略,没有的话使用默认策略
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/core/config.js
export default ({
  /**
   * Option merge strategies (used in core/util/options)
   */
  optionMergeStrategies: Object.create(null)
}
1
2
3
4
5
6
7

根据选项策略的不同,我们将这些选项分为以下几类:

  • dataprovide选项
  • 生命周期钩子函数
  • 资源选项(componentdirectivefilter
  • watch选项
  • propsmethodsinjectcomputed选项
  • 默认合并策略
  • (开发模式)elpropsData选项

BTW,以下涉及的代码主要在src/core/util/options.js文件里。

data、provide 选项

按以下步骤来合并data数据(以下的childparent代表的是子/父配置的data选项的值):

  1. childparent数据若是函数,则执行函数获得返回的对象
  2. parent不存在,返回child
  3. child不存在,返回返回parent
  4. 遍历parent的属性key
    • child[key]不存在,则child[key]取用parent[key]的值
    • child[key]存在,且child[key]parent[key]都是对象,则递归合并child[key]parent[key]

这里需要注意的是,合并配置时vm不存在(即是调用Vue.extendVue.mixin合并配置生成构造函数的options时)和vm存在(生成组件实例的$options时)的情况略微有些不同。

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // vm 不存在,即调用`Vue.extend`、`Vue.mixin`合并配置生成构造函数的`options`时
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }

    // 返回一个 data function,等真正实例化的时候再调用
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    // vm 存在,即生成组件实例的`$options`时
    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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

provide选项类似data选项。

strats.provide = mergeDataOrFn
1

生命周期钩子函数

同一生命周期钩子合并成一数组,并且parent的钩子排在数组前面,会优先执行。

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]
1
2
3
4
5
6
7
8
9
10
11
12
13
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
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

资源选项(component、directive、filter)

childoptions的资源的每一项覆盖parentoptions的资源的每一项。

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
1
2
3
4
5
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
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

watch

  1. childwatch选项不存在,则采用parentwatch选项
  2. parentwatch选项不存在,则采用childwatch选项不存在
  3. 若都存在,将同名的 Watcher 合并成数组,且parent的 Watcher 排在前面优先执行(单个的 Watcher 也以数组形式返回)
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

props、methods、inject、computed 选项

存在同名的key,则child将覆盖parent

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

默认合并策略

如果 childVal 存在则使用 childVal,否则使用 parentVal

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
1
2
3
4
5

(开发模式)el、propsData 选项

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)
  }
}
1
2
3
4
5
6
7
8
9
10
11

将 config 挂载在 Vue 上

经过以上的分析,各种选项的策略函数都存在在config.optionMergeStrategies对象上,且最终config对象会挂载在Vue上,以供开发者使用。

// src/core/global-api/index.js
import config from '../config'
// ...
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

因此,开发者可通过Vue.config.optionMergeStrategies来获取或自定义添加选项的合并策略函数了,比如const mountedMergeFn = Vue.config.optionMergeStrategies.mounted来获取mounted钩子函数的合并策略函数。