Vue 初始化之合并 Options

2022/11/7 Vue

上一篇说到了Vue初始化,初始化的过程中有一个合并Options的过程,今天来看一下Vue为啥要合并options,以及是怎么初始化的

...
// 合并 options
// 是子组件时,_isComponent 为true
if (options && options._isComponent) {
  // 优化内部组件实例化,因为动态选项合并非常慢,而且没有任何内部组件 options 需要特殊处理。
  initInternalComponent(vm, options as any)
} else {
  // Vue 实例是根实例
  // 就会给$options 属性赋值一个 合并的options
  vm.$options = mergeOptions(
    // 解析constructor上的options属性的
    resolveConstructorOptions(vm.constructor as any),
    options || {}, // 传入的 options
    vm // 该实例 this
  )
}
...

上面的代码是_init方法里的,Vue 在初始化的时候,会根据外部创建实例还是内部组件实例来不同的处理这些,如果是内部组件实例(_isComponent)来处理的时候,会走initInternalComponent 这个方法,把父组件的一些属性挂载到$options上。如果用户主动new创建的实例,则走resolveConstructorOptions这个方法,这个就是将Vue默认配置与用户自定义的options进行合并成一个新的Object,并赋值给实例vm的属性$options。

# 1、resolveConstructorOptions

new Vue() 的时候, 会先走else的分支,所以我们先看resolveConstructorOptions这个方法

// 返回类的构造函数上最新的options值
// 参数是实例的构造函数 vm.constructor
export function resolveConstructorOptions(Ctor: typeof Component) {
  let options = Ctor.options
  // 根实例没有 super,extend 生成的子组件是有
  // 如果有 super
  if (Ctor.super) {
    // 寻找父类的 options
    // 通过递归,把上一级父类上最新的 options 更新到变量 superOptions 中。
    const superOptions = resolveConstructorOptions(Ctor.super)
    // 把 extend 时父类的 options,即 Ctor.superOptions 赋值给 cachedSuperOptions 变量。
    const cachedSuperOptions = Ctor.superOptions
    // 如果父类的 options 发生了改变
    // 可能是被全局 mixins 混入了其他内容
    if (superOptions !== cachedSuperOptions) {
      // 更新 options
      Ctor.superOptions = superOptions
      // 判断现有子类 option 和在 extend 中挂载到子类 option 中的 sealedOptions是否一样
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 更新 extendOptions
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 然后获取最新的 options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  // 返回 global-api 里设置的 vue.options
  return options
}

resolveConstructorOption 方法只接受一个参数Ctor,调用的时候传的参数是vm.constructor,通常情况下这个值指向的其实就是Vue构造函数本身。

所以在通常情况下,resolveConstructorOption方法就单纯的返回 Vue.options

但是在 if 分支是做什么的呢?

if (Ctor.super) {
  ...
}

if语句的判断条件是Ctor.super,而super是子类才有的属性,那怎么会出现这种情况呢?

// src/core/global-api/extend.ts
Vue.extend = function (extendOptions: any): typeof Component {
  ...
}

Vue.extend 的方法是Vue组件化的基础。Vue 的 extend方法,它返回的是一个扩展实例构造器,预设了部分选项的Vue实例构造器,但没有实例化,其实就是创建一个子类,然后继承Vue的一些功能。

所以在子组件的时候,会走if分支,那么if分支是做什么的呢?

if (Ctor.super) {
  // 寻找父类的 options
  // 通过递归,把上一级父类上最新的 options 更新到变量 superOptions 中。
  const superOptions = resolveConstructorOptions(Ctor.super)
  // 把 extend 时父类的 options,即 Ctor.superOptions 赋值给 cachedSuperOptions 变量。
  const cachedSuperOptions = Ctor.superOptions
  // 如果父类的 options 发生了改变
  // 可能是被全局 mixins 混入了其他内容
  if (superOptions !== cachedSuperOptions) {
    // 更新 options
    Ctor.superOptions = superOptions
    // 判断现有子类 option 和在 extend 中挂载到子类 option 中的 sealedOptions是否一样
    const modifiedOptions = resolveModifiedOptions(Ctor)
    // 更新 extendOptions
    if (modifiedOptions) {
      extend(Ctor.extendOptions, modifiedOptions)
    }
    // 然后获取最新的 options
    options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
    if (options.name) {
      options.components[options.name] = Ctor
    }
  }
}

在if语句中,作为子组件,就要先递归找到父类的options,然后判断父类的options有没有被mixins 混入其他的信息。最后拿到最新的options,返回最新的options

走完resolveConstructorOption方法,就会进入 mergeOptions 方法

# 2、mergeOptions

/**
 * 将两个选项对象合并为一个新的选项对象。
 * 核心工具函数,在实例化和继承中使用。
 */
export function mergeOptions(
  parent: Record<string, any>,
  child: Record<string, any>,
  vm?: Component | null
): ComponentOptions {
  // 在开发环境下使用checkComponents
  // 检测自定义配置中的组件名称命名是否规范
  if (__DEV__) {
    checkComponents(child)
  }

  // 判断 child 是否是一个函数
  // 若是函数,则child重新指向它的静态属性child.options
  if (isFunction(child)) {
    // @ts-expect-error
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  /*
  * 在子选项上应用extends和mixins,但前提是它是一个原始选项对象,
  * 不是另一个mergeOptions调用的结果。 
  * 只有合并选项具有_base属性。
  */
  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: ComponentOptions = {} as any
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField(key: any) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions方法就是把两个options合并成一个新的options。

# 2.1、checkComponents

首先是在非生产环境的情况下,执行checkComponents方法。这个方法就是检查我们options里的name属性是否合法。

/**
 * 校验组件名称
 */
function checkComponents(options: Record<string, any>) {
  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
    )
  }
}

检测传递options的components属性。

  • 规定组件的命名规则由普通的字符和中横线(-)组成,且必须以字母开头。
  • 组件名称不能与内置标签(slot、component)冲突,也不能是内置标签。

# 2.2、判断初始化 option 是否为函数

紧接着,判断一下new Vue 时用户传入的options,如果传入的options是一个函数,则让 child = child.options

// src/core/util/options.ts
if (isFunction(child)) {
  // @ts-expect-error
  child = child.options
}

// src/shared/util.ts
export function isFunction(value: any): value is (...args: any[]) => any {
  return typeof value === 'function'
}

# 2.3、规范props,inject 和 directives

判断完上面的信息之后,就会走三个normalize方法,分别规范用户传入options里的 props、inject 和 directives。

...

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

...

# 2.3.1、normalizeProps

/**
 * 确保所有props选项语法都规范化为基于对象的格式。
 */
function normalizeProps(options: Record<string, any>, vm?: Component | null) {
  const props = options.props
  if (!props) return
  const res: Record<string, any> = {}
  let i, val, name
  if (isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        // 把用连字符分隔的字符串换成驼峰形式.
        name = camelize(val)
        // 在 res 里添加该 name 的 props 对象
        res[name] = { type: null }
      } else if (__DEV__) {
        // 测试环境给提醒,数组类型的 props,元素必须是字符串类型
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // 如果是对象
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      // 赋值
      res[name] = isPlainObject(val) ? val : { type: val }
    }
  } else if (__DEV__) {
    // 报错
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
/**
 * 把用连字符分隔的字符串换成驼峰形式.
 */
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

这个normalizeProps方法,就做了一件事,就是把各种类型的props写法,转成对象形式的,并且把props的name 转成小驼峰形式,增加一个type属性,若有指定类型则显示类型,否则为null。然后把res赋值给options的props。

三个 if 语句,分别对应数组类型,对象类型还有报错。

# 2.3.2、normalizeInject

/**
 * 把 inject 规范成对象类型
 */
function normalizeInject(options: Record<string, any>, vm?: Component | null) {
  // 定义变量inject缓存options.inject的值
  const inject = options.inject
  // 没有 inject 返回
  if (!inject) return
  // 定义normalized与options.inject同时指向一个空对象
  const normalized: Record<string, any> = (options.inject = {})

  // 数组类型
  if (isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? // 如果是对象,就新增一个 {from: key }
          extend({ from: key }, val)
        : { from: val }
    }
  } else if (__DEV__) {
    // 报错
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

看完了 normalizeProps 再看 normalizeInject,就很简单了,也是修改 inject 的形式,主要做了2种形式的调整

  • 若是数组格式,则遍历改成{ key: { from: key } }格式;
  • 若是对象,如果val是对象,则改成{ key: { from: key,data: data } }格式;否则还是{ key: { from: key } }格式

# 2.3.3、normalizeDirectives

/**
 * 将原始函数指令规范化为对象格式.
 */
function normalizeDirectives(options: Record<string, any>) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (isFunction(def)) {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

该项的规范化就是把只有一个value的值的 directives,给添加bind 和 update 对象形式

// 修改前
{
  directives:{
    a:function(){}
  }
}
//修改后
{
  directives:{
    a:{
      bind:function(){},
      update:function(){}
    }
  }
}

上面的规范化的过程结束,这部分主要是为了后面的options的合并做准备。

# 2.4、处理extends和mixins

/*
   * 在子选项上应用extends和mixins,但前提是它是一个原始选项对象,
   * 不是另一个mergeOptions调用的结果。
   * 只有合并选项具有_base属性。
   */
  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)
      }
    }
  }

_base是Vue.config默认配置的属性,指向的是构造函数Vue,合并时,会将_base合并在一起,合并后 options 就会有_base了。

该if 分支,只有在初次合并而不是mergeOptions的结果再合并的时候,才会执行。

目的就是把用户options中可能存在的mixins、extends等合并到parent中。

# 2.5、选项的合并

...

const options: ComponentOptions = {} as any
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
function mergeField(key: any) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
return options

经历了组件校验,规范化属性,合并mixins等一系列的操作,终于进入了合并阶段。

这一阶段大概就进行了3步

  • 把parent 的属性通过mergeField放到options里
  • 把child 里的parent里没有的属性通过mergeField放到options里
  • 返回options

这部分采用了策略模式来处理不同的合并情况:

src/core/util/options.ts文件在上面声明strats对象,定义了el、propsData、data、lifecycle、components、directives、filters、watch、props、methods、inject、computed、provide的合并方式,不同的属性对应不同的策略。还有一个defaultStrat, 来处理用户自定义的属性。

strats 是 config.optionMergeStrategies,这个属性是选项合并策略 (opens new window),用户可以在初始化的时候自定义合并策略,如果child 里有自定义的合并策略,那就会走自定义的策略。

下面我们就分别看看不同属性对应的合并策略:

# 2.5.1、默认合并策略

/**
 * 默认合并策略
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal
}

默认策略比较简单,就是判断 child 的值不为 undefined,就使用 child的值,否则就是 parent 的值

# 2.5.2、el 和 propsData 合并策略

if (__DEV__) {
  strats.el = strats.propsData = function (
    parent: any,
    child: any,
    vm: any,
    key: any
  ) {
    // 提示el选项或者propsData选项只能在使用new操作符创建实例的时候可用
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
          'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

el 和 propsData 的合并策略就是默认的合并策略,在非生产环境下有这两个属性的合并策略,主要是为了给出提示。在生产环节则不执行该策略,直接走默认策略。

# 2.5.3、data 和 provide 合并策略

...
// data
strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): Function | null {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      __DEV__ &&
        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)
}

// provide
strats.provide = mergeDataOrFn
...

data 和 provide 都使用了 mergeDataOrFn 这个方法来处理合并策略。

但是 provide 合并时是传了三个参数:父选项parentVal、子选项childVal、实例vm。

选项 data 的合并分三种情况:

  • 通过Vue.extends()处理的子组件的 options,且传入的options里的data不是函数的情况下(data必须是函数)则不做合并处理,直接返回parentVal结果
  • 通过Vue.extends()处理的子组件options时,且options里的data是一个函数,使用mergeDataOrFn处理data
  • 正常实例化(new Vue)时,这个时候有vm实例,则使用mergeDataOrFn处理data

我们看一下mergeDataOrFn是怎么处理的

/**
 * Data
 */
export function mergeDataOrFn(
  parentVal: any,
  childVal: any,
  vm?: Component
): Function | null {
  // 如果没有实例
  if (!vm) {
    // Vue.extend 的子组件合并,两者都应该是函数
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // 当parentVal和childVal都存在时,
    // 我们需要返回一个函数,该函数返回两个函数的合并结果…
    // 这里不需要检查parentVal是否是一个函数,
    // 因为它必须是一个传递先前合并的函数。
    return function mergedDataFn() {
      return mergeData(
        isFunction(childVal) ? childVal.call(this, this) : childVal,
        isFunction(parentVal) ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    // 处理的是非子组件options,也就是处理new操作符创建实例的情况
    // 返回的一个函数
    return function mergedInstanceDataFn() {
      // 执行 childVal 方法,返回纯对象 instanceData
      const instanceData = isFunction(childVal)
        ? childVal.call(vm, vm)
        : childVal
      // 执行 parentVal 方法,返回纯对象 defaultData
      const defaultData = isFunction(parentVal)
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

mergeDataOrFn方法永远返回一个函数,区别在于,没有有实例的时候,返回mergedDataFn,有实例的时候返回mergedInstanceDataFn。内部只是调用了mergeData方法,将父子data函数执行后得到的纯对象合并之后得到合并后的纯对象

而mergeData是怎么处理的呢?

/**
 * 递归地将两个数据对象合并在一起的helper.
 */
function mergeData(
  to: Record<string | symbol, any>,
  from: Record<string | symbol, any> | null
): Record<PropertyKey, any> {
  // 没有 from 直接返回 to
  if (!from) return to
  let key, toVal, fromVal

  // 判断是否支持 symbol,使用不通的方法出 keys
  const keys = hasSymbol
    ? (Reflect.ownKeys(from) as string[])
    : Object.keys(from)

  // 遍历keys
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // 如果 object 已经被观察,就跳过...
    if (key === '__ob__') continue

    toVal = to[key]
    fromVal = from[key]
    // 如果from对象中的key值不存在to中,则调用set函数对to设置对应的值
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      // 如果不全等
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 递归深度合并
      mergeData(toVal, fromVal)
    }
  }
  return to
}

mergeData 方法就是拿到了所有key,然后遍历,调用 src/core/observer/index.ts里的set方法来合并。

# 2.5.4、Lifecycle 合并策略

/**
 * 钩子函数 和 props 将被合并成数组.
 */
export function mergeLifecycleHook(
  parentVal: Array<Function> | null,
  childVal: Function | Array<Function> | null
): Array<Function> | null {
  // 这三组三目运算符看着真费劲…
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal

  // 这段内容和上面一样
  // let res
  // if (!childVal) {
  //   res = parentVal
  // } else {
  //   if (parentVal) {
  //     res = parentVal.concat(childVal)
  //   } else {
  //     if (isArray(childVal)) {
  //       res = childVal
  //     }else{
  //       res = [childVal]
  //     }
  //   }
  // }

  // 如果res 存在,那就添加到数组里
  return res ? dedupeHooks(res) : res
}

function dedupeHooks(hooks: any) {
  const res: Array<any> = []
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i])
    }
  }
  return res
}
// 遍历数组,每个钩子函数都对应 mergeLifecycleHook 函数
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeLifecycleHook
})

// src/shared/constants.ts
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch',
  'renderTracked',
  'renderTriggered'
] as const

生命周期合并,采用的策略是:把所有的生命周期钩子都合并成一个数组,每一个元素都会保留。parentval的钩子函数在前,子组建的函数在后。在执行的时候,也会按照这个顺序。

# 2.5.5 assets 合并策略

// src/shared/constants.ts
export const ASSET_TYPES = ['component', 'directive', 'filter'] as const

根据上面的代码,我们看到 assets 的 types 包含:components、directives、filters 这三种。这三个属性都可以作为第三方应用来提供。

而这些 assets 的合并策略也很简单

/**
 * Assets
 * 当vm存在时(创建实例),我们需要执行以下操作 
 * 构造函数选项之间的三向合并选项和父选项。
 */
function mergeAssets(
  parentVal: Object | null,
  childVal: Object | null,
  vm: Component | null,
  key: string
): Object {
  // 定义合并后选项为空对象。如果parentVal存在,则以parentVal为原型,否则没有原型
  const res = Object.create(parentVal || null)
  if (childVal) {
    // 如果childVal为纯对象,则将childVal上的各属性复制到合并后的选项对象上。
    // 非生产环境,如果非纯对象,则给报错提示
    __DEV__ && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

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

所以资源的合并策略就是先创建一个parentVal为原型的空对象,将childVal的属性合并到空对象后返回。

# 2.5.6 watch 合并策略

/**
 * Watchers.
 *
 * watchers 不应该相互覆盖,因此我们将它们合并为数组。
 */
strats.watch = function (
  parentVal: Record<string, any> | null,
  childVal: Record<string, any> | null,
  vm: Component | null,
  key: string
): Object | null {
  // 首先确保父子的watch都不是火狐浏览器对象原型链上的watch,若是,则置空。
  //@ts-expect-error work around
  if (parentVal === nativeWatch) parentVal = undefined
  //@ts-expect-error work around
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  // 若子类 watch 不存在,则直接返回原型为父类的空对象
  if (!childVal) return Object.create(parentVal || null)
  if (__DEV__) {
    // 检测子类 watch 是否是纯对象,若不是则给出警告;
    assertObjectType(key, childVal, vm)
  }
  // 没有父类 watch,则返回 childVal
  if (!parentVal) return childVal
  // 声明一个空对象 ret
  const ret: Record<string, any> = {}
  // 把父类的值赋值给 ret
  extend(ret, parentVal)
  // 遍历子类
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    // 检测父类是否包含同名选项,若有则需确保父类同名选项为数组格式
    if (parent && !isArray(parent)) {
      parent = [parent]
    }
    // 最后将子类每一项复制到ret对象中,
    // 若与父类名称冲突则返回与父类合并后的新数组,
    // 若不冲突则返回当前选项元素组成的新数组
    ret[key] = parent ? parent.concat(child) : isArray(child) ? child : [child]
  }
  return ret
}

合并watch,就是要返回一个对象,对象里的每个属性,都是一个数组或者函数。在合并的watch时,若父子存在同名的watch,则同名元素的值为数组格式,否则返回原来的函数。

# 2.5.7 props、methods、inject、computed 合并策略

这几个属性的合并策略是同一个方法,选项 methods 与 computed 传入时只接受对象形式,而选项 props 与 inject 经过前面的规范之后也是纯对象的形式

/**
 * Other object hashes.
 */
strats.props =
  strats.methods =
  strats.inject =
  strats.computed =
    function (
      parentVal: Object | null,
      childVal: Object | null,
      vm: Component | null,
      key: string
    ): Object | null {
      if (childVal && __DEV__) {
        assertObjectType(key, childVal, vm)
      }
      if (!parentVal) return childVal
      const ret = Object.create(null)
      extend(ret, parentVal)
      if (childVal) extend(ret, childVal)
      return ret
    }

这四个选项的合并策略,是创建一个没有原型的对象,然后遍历拷贝父子类到新对象,并返回新对象。

# initInternalComponent

上面我们看了用户主动 new Vue来创建实例的情况,现在来看看Vue 内部组件在合并options的时候的情况。

// 初始化合并选项
// 主要做两件事: 1、指定组件 $options 原型
//             2、把组件依赖于父组件的props、listeners也挂载到options上,方便子组件调用
export function initInternalComponent(
  vm: Component, // 组件实例 即this
  options: InternalComponentOptions // createComponentInstanceForVnode中定义的组件options
) {
  // 把组件构造函数的 options 挂载到 vm.$options 的 __proto__ 上
  const opts = (vm.$options = Object.create((vm.constructor as any).options))
  // 把传入参数的 option 的 _parentVode 和 parent 挂载到组件实例 $options 上
  // 这样做是因为它比动态枚举更快.
  const parentVnode = options._parentVnode // 组件 Vnode 对象
  opts.parent = options.parent // 根实例
  opts._parentVnode = parentVnode

  // 父组件里的 vnode 上的
  // propsData,listeners,children,tag属性挂载到 $options 上
  const vnodeComponentOptions = parentVnode.componentOptions!
  opts.propsData = vnodeComponentOptions.propsData // props
  opts._parentListeners = vnodeComponentOptions.listeners // 子组件emit出来的方法
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  // 如果传入的 option 中如果有 render,把render相关的也挂载到 $options 上
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

通过上面的代码可以看出:

  • initInternalComponent先执行 const opts = (vm.$options = Object.create((vm.constructor as any).options)),这里的 vm.constructor 就是子组件的构造函数,相当于Vue.extends()出来的构造函数,相当于 vm.$options = Object.create(Vue.extends().options)

  • 然后把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。

所以 initInternalComponent 只是做了简单对象赋值,不涉及到规范化属性,合并策略等逻辑。

# 总结

options 合并有 2 种方式,子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,而mergeOptions则要处理用户初始化的属性的规范化,合并策略等。最后所有的options,合并完的结果保留在 vm.$options 中。