]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: adjust component options merge cache strategy
authorEvan You <yyx990803@gmail.com>
Wed, 2 Jun 2021 14:37:50 +0000 (10:37 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 2 Jun 2021 14:42:52 +0000 (10:42 -0400)
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
packages/runtime-core/src/compat/instance.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentPublicInstance.ts

index a6736b797bff2dacfde43f019c7e1ee91ee25aae..6e3405595b0fd6145b0d50f6ab70d65ea6a1adeb 100644 (file)
@@ -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
@@ -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
@@ -137,7 +139,8 @@ export function createAppContext(): AppContext {
     mixins: [],
     components: {},
     directives: {},
-    provides: Object.create(null)
+    provides: Object.create(null),
+    cache: new WeakMap()
   }
 }
 
index 966a9a1a655eb54605f3e86ebf4b3519d4bd02ce..39f27f59655aaa50e3225a7d29db5ddb93e6fddb 100644 (file)
@@ -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,
index e467ecbc8689ba1a4b702770b346a3698ba7717b..51d9c83a491169fc3ddd42ca663c137d070945ff 100644 (file)
@@ -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<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<
@@ -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<string, OptionMergeFunction>
 ) {
   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]
     }
index 9610aad19b452b7d88fa0023fa69f88ecf413aeb..493b7f131f901f600daa1a3c096aa8a096925929 100644 (file)
@@ -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<E>
   $el: any
-  $options: Options
+  $options: Options & MergedComponentOptionsOverride
   $forceUpdate: ReactiveEffect
   $nextTick: typeof nextTick
   $watch(