From 1e35a860b995c1158d5c4e1706d2fc9bcd3b8412 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 2 Jun 2021 10:37:50 -0400 Subject: [PATCH] refactor: adjust component options merge cache strategy 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. --- packages/runtime-core/src/apiCreateApp.ts | 17 ++-- packages/runtime-core/src/compat/instance.ts | 11 --- packages/runtime-core/src/componentOptions.ts | 96 ++++++++++++++----- .../src/componentPublicInstance.ts | 5 +- 4 files changed, 86 insertions(+), 43 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index a6736b797..6e3405595 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -53,12 +53,7 @@ export interface App { _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 @@ -97,6 +92,13 @@ export interface AppContext { components: Record directives: Record provides: Record + + /** + * 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 /** * Flag for de-optimizing props normalization * @internal @@ -137,7 +139,8 @@ export function createAppContext(): AppContext { mixins: [], components: {}, directives: {}, - provides: Object.create(null) + provides: Object.create(null), + cache: new WeakMap() } } diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 966a9a1a6..39f27f596 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -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' @@ -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, diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index e467ecbc8..51d9c83a4 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -79,6 +79,7 @@ import { DIRECTIVES, FILTERS } from './helpers/resolveAssets' +import { OptionMergeFunction } from './apiCreateApp' /** * Interface for declaring custom options. @@ -194,11 +195,6 @@ export interface ComponentOptionsBase< * @internal */ __asyncResolved?: ConcreteComponent - /** - * cache for merged $options - * @internal - */ - __merged?: ComponentOptions // Type differentiators ------------------------------------------------------ @@ -486,6 +482,28 @@ interface LegacyOptions< __differentiator?: keyof D | keyof C | keyof M } +type MergedHook 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 + renderTriggered?: MergedHook + errorCaptured?: MergedHook +} + export type OptionTypesKeys = 'P' | 'B' | 'D' | 'C' | 'M' | 'Defaults' export type OptionTypesType< @@ -1022,25 +1040,56 @@ 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 ) { if (__COMPAT__ && isFunction(from)) { from = from.options @@ -1048,15 +1097,16 @@ export function mergeOptions( 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] } diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 9610aad19..493b7f131 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -33,7 +33,8 @@ import { OptionTypesType, OptionTypesKeys, resolveMergedOptions, - shouldCacheAccess + shouldCacheAccess, + MergedComponentOptionsOverride } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { Slots } from './componentSlots' @@ -188,7 +189,7 @@ export type ComponentPublicInstance< $parent: ComponentPublicInstance | null $emit: EmitFn $el: any - $options: Options + $options: Options & MergedComponentOptionsOverride $forceUpdate: ReactiveEffect $nextTick: typeof nextTick $watch(