Skip to content

Commit

Permalink
refactor: adjust component options merge cache strategy
Browse files Browse the repository at this point in the history
BREAKING CHANGE: optionMergeStrategies functions no longer receive
the component instance as the 3rd argument. The argument was technically
internal in Vue 2 and only used for generating warnings, and should not
be needed in userland code. This removal enables much more efficient
caching of option merging.
  • Loading branch information
yyx990803 committed Jun 2, 2021
1 parent 44996d1 commit 1e35a86
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 43 deletions.
17 changes: 10 additions & 7 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,7 @@ export interface App<HostElement = any> {
_createRoot?(options: ComponentOptions): ComponentPublicInstance
}

export type OptionMergeFunction = (
to: unknown,
from: unknown,
instance: any,
key: string
) => any
export type OptionMergeFunction = (to: unknown, from: unknown) => any

export interface AppConfig {
// @private
Expand Down Expand Up @@ -97,6 +92,13 @@ export interface AppContext {
components: Record<string, Component>
directives: Record<string, Directive>
provides: Record<string | symbol, any>

/**
* Cache for merged/normalized component options
* Each app instance has its own cache because app-level global mixins and
* optionMergeStrategies can affect merge behavior.
*/
cache: WeakMap<ComponentOptions, ComponentOptions>
/**
* Flag for de-optimizing props normalization
* @internal
Expand Down Expand Up @@ -137,7 +139,8 @@ export function createAppContext(): AppContext {
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
provides: Object.create(null),
cache: new WeakMap()
}
}

Expand Down
11 changes: 0 additions & 11 deletions packages/runtime-core/src/compat/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
legacyresolveScopedSlots
} from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions'
import { InternalSlots, Slots } from '../componentSlots'
import { ContextualRenderFn } from '../componentRenderContext'

Expand Down Expand Up @@ -128,16 +127,6 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
// needed by many libs / render fns
$vnode: i => i.vnode,

// inject addtional properties into $options for compat
// e.g. vuex needs this.$options.parent
$options: i => {
let res = resolveMergedOptions(i)
if (res === i.type) res = i.type.__merged = extend({}, res)
res.parent = i.proxy!.$parent
res.propsData = i.vnode.props
return res
},

// some private properties that are likely accessed...
_self: i => i.proxy,
_uid: i => i.uid,
Expand Down
96 changes: 73 additions & 23 deletions packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
DIRECTIVES,
FILTERS
} from './helpers/resolveAssets'
import { OptionMergeFunction } from './apiCreateApp'

/**
* Interface for declaring custom options.
Expand Down Expand Up @@ -194,11 +195,6 @@ export interface ComponentOptionsBase<
* @internal
*/
__asyncResolved?: ConcreteComponent
/**
* cache for merged $options
* @internal
*/
__merged?: ComponentOptions

// Type differentiators ------------------------------------------------------

Expand Down Expand Up @@ -486,6 +482,28 @@ interface LegacyOptions<
__differentiator?: keyof D | keyof C | keyof M
}

type MergedHook<T = (() => void)> = T | T[]

export type MergedComponentOptionsOverride = {
beforeCreate?: MergedHook
created?: MergedHook
beforeMount?: MergedHook
mounted?: MergedHook
beforeUpdate?: MergedHook
updated?: MergedHook
activated?: MergedHook
deactivated?: MergedHook
/** @deprecated use `beforeUnmount` instead */
beforeDestroy?: MergedHook
beforeUnmount?: MergedHook
/** @deprecated use `unmounted` instead */
destroyed?: MergedHook
unmounted?: MergedHook
renderTracked?: MergedHook<DebuggerHook>
renderTriggered?: MergedHook<DebuggerHook>
errorCaptured?: MergedHook<ErrorCapturedHook>
}

export type OptionTypesKeys = 'P' | 'B' | 'D' | 'C' | 'M' | 'Defaults'

export type OptionTypesType<
Expand Down Expand Up @@ -1022,41 +1040,73 @@ export function createWatcher(
}
}

/**
* Resolve merged options and cache it on the component.
* This is done only once per-component since the merging does not involve
* instances.
*/
export function resolveMergedOptions(
instance: ComponentInternalInstance
): ComponentOptions {
const raw = instance.type as ComponentOptions
const { __merged, mixins, extends: extendsOptions } = raw
if (__merged) return __merged
const globalMixins = instance.appContext.mixins
if (!globalMixins.length && !mixins && !extendsOptions) return raw
const options = {}
globalMixins.forEach(m => mergeOptions(options, m, instance))
mergeOptions(options, raw, instance)
return (raw.__merged = options)
): ComponentOptions & MergedComponentOptionsOverride {
const base = instance.type as ComponentOptions
const { mixins, extends: extendsOptions } = base
const {
mixins: globalMixins,
cache,
config: { optionMergeStrategies }
} = instance.appContext
const cached = cache.get(base)

let resolved: ComponentOptions

if (cached) {
resolved = cached
} else if (!globalMixins.length && !mixins && !extendsOptions) {
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance)
) {
resolved = extend({}, base)
resolved.parent = instance.parent && instance.parent.proxy
resolved.propsData = instance.vnode.props
} else {
resolved = base
}
} else {
resolved = {}
if (globalMixins.length) {
globalMixins.forEach(m =>
mergeOptions(resolved, m, optionMergeStrategies)
)
}
mergeOptions(resolved, base, optionMergeStrategies)
}

cache.set(base, resolved)
return resolved
}

export function mergeOptions(
to: any,
from: any,
instance?: ComponentInternalInstance | null,
strats = instance && instance.appContext.config.optionMergeStrategies
strats: Record<string, OptionMergeFunction>
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
}

const { mixins, extends: extendsOptions } = from

extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins &&
mixins.forEach((m: ComponentOptionsMixin) =>
mergeOptions(to, m, instance, strats)
)
if (extendsOptions) {
mergeOptions(to, extendsOptions, strats)
}
if (mixins) {
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, strats))
}

for (const key in from) {
if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
to[key] = strats[key](to[key], from[key])
} else {
to[key] = from[key]
}
Expand Down
5 changes: 3 additions & 2 deletions packages/runtime-core/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import {
OptionTypesType,
OptionTypesKeys,
resolveMergedOptions,
shouldCacheAccess
shouldCacheAccess,
MergedComponentOptionsOverride
} from './componentOptions'
import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots'
Expand Down Expand Up @@ -188,7 +189,7 @@ export type ComponentPublicInstance<
$parent: ComponentPublicInstance | null
$emit: EmitFn<E>
$el: any
$options: Options
$options: Options & MergedComponentOptionsOverride
$forceUpdate: ReactiveEffect
$nextTick: typeof nextTick
$watch(
Expand Down

0 comments on commit 1e35a86

Please sign in to comment.