Vue 初始化之合并 Options
上一篇说到了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 中。