]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): align option merge behavior with Vue 2
authorEvan You <yyx990803@gmail.com>
Wed, 2 Jun 2021 18:37:27 +0000 (14:37 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 2 Jun 2021 19:19:56 +0000 (15:19 -0400)
fix #3566, #2791

packages/runtime-core/__tests__/apiOptions.spec.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/compat/compatConfig.ts
packages/runtime-core/src/compat/data.ts
packages/runtime-core/src/compat/global.ts
packages/runtime-core/src/compat/globalConfig.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/helpers/resolveAssets.ts

index 9da4e7b03d0d898efa47ee5fdee16745e7ab9338..87d57afc91d815eea63f2811af1c191e2fd691aa 100644 (file)
@@ -1066,6 +1066,188 @@ describe('api: options', () => {
     )
   })
 
+  describe('options merge strategies', () => {
+    test('this.$options.data', () => {
+      const mixin = {
+        data() {
+          return { foo: 1, bar: 2 }
+        }
+      }
+      createApp({
+        mixins: [mixin],
+        data() {
+          return {
+            foo: 3,
+            baz: 4
+          }
+        },
+        created() {
+          expect(this.$options.data).toBeInstanceOf(Function)
+          expect(this.$options.data()).toEqual({
+            foo: 3,
+            bar: 2,
+            baz: 4
+          })
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options.inject', () => {
+      const mixin = {
+        inject: ['a']
+      }
+      const app = createApp({
+        mixins: [mixin],
+        inject: { b: 'b', c: { from: 'd' } },
+        created() {
+          expect(this.$options.inject.a).toEqual('a')
+          expect(this.$options.inject.b).toEqual('b')
+          expect(this.$options.inject.c).toEqual({ from: 'd' })
+          expect(this.a).toBe(1)
+          expect(this.b).toBe(2)
+          expect(this.c).toBe(3)
+        },
+        render: () => null
+      })
+
+      app.provide('a', 1)
+      app.provide('b', 2)
+      app.provide('d', 3)
+      app.mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options.provide', () => {
+      const mixin = {
+        provide: {
+          a: 1
+        }
+      }
+      createApp({
+        mixins: [mixin],
+        provide() {
+          return {
+            b: 2
+          }
+        },
+        created() {
+          expect(this.$options.provide).toBeInstanceOf(Function)
+          expect(this.$options.provide()).toEqual({ a: 1, b: 2 })
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options[lifecycle-name]', () => {
+      const mixin = {
+        mounted() {}
+      }
+      createApp({
+        mixins: [mixin],
+        mounted() {},
+        created() {
+          expect(this.$options.mounted).toBeInstanceOf(Array)
+          expect(this.$options.mounted.length).toBe(2)
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options[asset-name]', () => {
+      const mixin = {
+        components: {
+          a: {}
+        },
+        directives: {
+          d1: {}
+        }
+      }
+      createApp({
+        mixins: [mixin],
+        components: {
+          b: {}
+        },
+        directives: {
+          d2: {}
+        },
+        created() {
+          expect('a' in this.$options.components).toBe(true)
+          expect('b' in this.$options.components).toBe(true)
+          expect('d1' in this.$options.directives).toBe(true)
+          expect('d2' in this.$options.directives).toBe(true)
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options.methods', () => {
+      const mixin = {
+        methods: {
+          fn1() {}
+        }
+      }
+      createApp({
+        mixins: [mixin],
+        methods: {
+          fn2() {}
+        },
+        created() {
+          expect(this.$options.methods.fn1).toBeInstanceOf(Function)
+          expect(this.$options.methods.fn2).toBeInstanceOf(Function)
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    test('this.$options.computed', () => {
+      const mixin = {
+        computed: {
+          c1() {}
+        }
+      }
+      createApp({
+        mixins: [mixin],
+        computed: {
+          c2() {}
+        },
+        created() {
+          expect(this.$options.computed.c1).toBeInstanceOf(Function)
+          expect(this.$options.computed.c2).toBeInstanceOf(Function)
+        },
+        render: () => null
+      }).mount(nodeOps.createElement('div'))
+    })
+
+    // #2791
+    test('modify $options in the beforeCreate hook', async () => {
+      const count = ref(0)
+      const mixin = {
+        data() {
+          return { foo: 1 }
+        },
+        beforeCreate(this: any) {
+          if (!this.$options.computed) {
+            this.$options.computed = {}
+          }
+          this.$options.computed.value = () => count.value
+        }
+      }
+      const root = nodeOps.createElement('div')
+      createApp({
+        mixins: [mixin],
+        render(this: any) {
+          return this.value
+        }
+      }).mount(root)
+
+      expect(serializeInner(root)).toBe('0')
+
+      count.value++
+      await nextTick()
+      expect(serializeInner(root)).toBe('1')
+    })
+  })
+
   describe('warnings', () => {
     test('Expected a function as watch handler', () => {
       const Comp = {
index 6e3405595b0fd6145b0d50f6ab70d65ea6a1adeb..617ddd20e384067bab09b8cc2aec701a3b07de02 100644 (file)
@@ -4,7 +4,11 @@ import {
   validateComponentName,
   Component
 } from './component'
-import { ComponentOptions, RuntimeCompilerOptions } from './componentOptions'
+import {
+  ComponentOptions,
+  MergedComponentOptions,
+  RuntimeCompilerOptions
+} from './componentOptions'
 import { ComponentPublicInstance } from './componentPublicInstance'
 import { Directive, validateDirectiveName } from './directives'
 import { RootRenderFunction } from './renderer'
@@ -98,7 +102,7 @@ export interface AppContext {
    * Each app instance has its own cache because app-level global mixins and
    * optionMergeStrategies can affect merge behavior.
    */
-  cache: WeakMap<ComponentOptions, ComponentOptions>
+  cache: WeakMap<ComponentOptions, MergedComponentOptions>
   /**
    * Flag for de-optimizing props normalization
    * @internal
index 9ca7de5c2c2a835d53acfe63154fdded75382e2c..f87831ee627fa64bf3fb540aedb85cb1396b8ae0 100644 (file)
@@ -531,7 +531,10 @@ const seenConfigObjects = /*#__PURE__*/ new WeakSet<CompatConfig>()
 const warnedInvalidKeys: Record<string, boolean> = {}
 
 // dev only
-export function validateCompatConfig(config: CompatConfig) {
+export function validateCompatConfig(
+  config: CompatConfig,
+  instance?: ComponentInternalInstance
+) {
   if (seenConfigObjects.has(config)) {
     return
   }
@@ -558,6 +561,14 @@ export function validateCompatConfig(config: CompatConfig) {
       warnedInvalidKeys[key] = true
     }
   }
+
+  if (instance && config[DeprecationTypes.OPTIONS_DATA_MERGE] != null) {
+    warn(
+      `Deprecation config "${
+        DeprecationTypes.OPTIONS_DATA_MERGE
+      }" can only be configured globally.`
+    )
+  }
 }
 
 export function getCompatConfigForKey(
index fa8b10858c541fb4d797ca1be9a308d7af99fff2..72c7314ee0f24be8283b9e053a987c7184f22c51 100644 (file)
@@ -1,39 +1,16 @@
-import { isFunction, isPlainObject } from '@vue/shared'
-import { ComponentInternalInstance } from '../component'
-import { ComponentPublicInstance } from '../componentPublicInstance'
+import { isPlainObject } from '@vue/shared'
 import { DeprecationTypes, warnDeprecation } from './compatConfig'
 
-export function deepMergeData(
-  to: any,
-  from: any,
-  instance: ComponentInternalInstance
-) {
+export function deepMergeData(to: any, from: any) {
   for (const key in from) {
     const toVal = to[key]
     const fromVal = from[key]
     if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) {
-      __DEV__ &&
-        warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, instance, key)
-      deepMergeData(toVal, fromVal, instance)
+      __DEV__ && warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, null, key)
+      deepMergeData(toVal, fromVal)
     } else {
       to[key] = fromVal
     }
   }
   return to
 }
-
-export function mergeDataOption(to: any, from: any) {
-  if (!from) {
-    return to
-  }
-  if (!to) {
-    return from
-  }
-  return function mergedDataFn(this: ComponentPublicInstance) {
-    return deepMergeData(
-      isFunction(to) ? to.call(this, this) : to,
-      isFunction(from) ? from.call(this, this) : from,
-      this.$
-    )
-  }
-}
index 3d518f4424eda70838422afc7a6c9494abe597b0..7823cc6a7e13641b94a29f720c642b83f98efb32 100644 (file)
@@ -34,7 +34,11 @@ import {
   isRuntimeOnly,
   setupComponent
 } from '../component'
-import { RenderFunction, mergeOptions } from '../componentOptions'
+import {
+  RenderFunction,
+  mergeOptions,
+  internalOptionMergeStrats
+} from '../componentOptions'
 import { ComponentPublicInstance } from '../componentPublicInstance'
 import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools'
 import { Directive } from '../directives'
@@ -43,8 +47,7 @@ import { version } from '..'
 import {
   installLegacyConfigWarnings,
   installLegacyOptionMergeStrats,
-  LegacyConfig,
-  legacyOptionMergeStrats
+  LegacyConfig
 } from './globalConfig'
 import { LegacyDirective } from './customDirective'
 import {
@@ -231,8 +234,7 @@ export function createCompatVue(
           mergeOptions(
             extend({}, SubVue.options),
             inlineOptions,
-            null,
-            legacyOptionMergeStrats as any
+            internalOptionMergeStrats as any
           ),
           SubVue
         )
@@ -257,8 +259,7 @@ export function createCompatVue(
     SubVue.options = mergeOptions(
       mergeBase,
       extendOptions,
-      null,
-      legacyOptionMergeStrats as any
+      internalOptionMergeStrats as any
     )
 
     SubVue.options._base = SubVue
@@ -305,8 +306,7 @@ export function createCompatVue(
       mergeOptions(
         parent,
         child,
-        vm && vm.$,
-        vm ? undefined : (legacyOptionMergeStrats as any)
+        vm ? undefined : (internalOptionMergeStrats as any)
       ),
     defineReactive
   }
index 593cd20e31beb47e7d80d9b7293d608b38c6c3ca..51f22ace60146275e1d6db06391a6517fe81908a 100644 (file)
@@ -1,12 +1,11 @@
-import { extend, isArray } from '@vue/shared'
 import { AppConfig } from '../apiCreateApp'
-import { mergeDataOption } from './data'
 import {
   DeprecationTypes,
   softAssertCompatEnabled,
   warnDeprecation
 } from './compatConfig'
 import { isCopyingConfig } from './global'
+import { internalOptionMergeStrats } from '../componentOptions'
 
 // legacy config warnings
 export type LegacyConfig = {
@@ -70,60 +69,16 @@ export function installLegacyOptionMergeStrats(config: AppConfig) {
         return target[key]
       }
       if (
-        key in legacyOptionMergeStrats &&
+        key in internalOptionMergeStrats &&
         softAssertCompatEnabled(
           DeprecationTypes.CONFIG_OPTION_MERGE_STRATS,
           null
         )
       ) {
-        return legacyOptionMergeStrats[
-          key as keyof typeof legacyOptionMergeStrats
+        return internalOptionMergeStrats[
+          key as keyof typeof internalOptionMergeStrats
         ]
       }
     }
   })
 }
-
-export const legacyOptionMergeStrats = {
-  data: mergeDataOption,
-  beforeCreate: mergeHook,
-  created: mergeHook,
-  beforeMount: mergeHook,
-  mounted: mergeHook,
-  beforeUpdate: mergeHook,
-  updated: mergeHook,
-  beforeDestroy: mergeHook,
-  destroyed: mergeHook,
-  activated: mergeHook,
-  deactivated: mergeHook,
-  errorCaptured: mergeHook,
-  serverPrefetch: mergeHook,
-  // assets
-  components: mergeObjectOptions,
-  directives: mergeObjectOptions,
-  filters: mergeObjectOptions,
-  // objects
-  props: mergeObjectOptions,
-  methods: mergeObjectOptions,
-  inject: mergeObjectOptions,
-  computed: mergeObjectOptions,
-  // watch has special merge behavior in v2, but isn't actually needed in v3.
-  // since we are only exposing these for compat and nobody should be relying
-  // on the watch-specific behavior, just expose the object merge strat.
-  watch: mergeObjectOptions
-}
-
-function toArray(target: any) {
-  return isArray(target) ? target : target ? [target] : []
-}
-
-function mergeHook(
-  to: Function[] | Function | undefined,
-  from: Function | Function[]
-) {
-  return Array.from(new Set([...toArray(to), ...toArray(from)]))
-}
-
-function mergeObjectOptions(to: Object | undefined, from: Object | undefined) {
-  return to ? extend(extend(Object.create(null), to), from) : from
-}
index ec1aa65b380ebac1589f3428a2c2639c38251f52..fd7b5777c93b3026007beac97a56de7adbc58a6f 100644 (file)
@@ -795,7 +795,7 @@ export function finishComponentSetup(
   if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
     currentInstance = instance
     pauseTracking()
-    applyOptions(instance, Component)
+    applyOptions(instance)
     resetTracking()
     currentInstance = null
   }
index 51d9c83a491169fc3ddd42ca663c137d070945ff..3983e8d94e71ed7be5cb3d8b7c1cc8827fedacef 100644 (file)
@@ -16,7 +16,6 @@ import {
   isArray,
   EMPTY_OBJ,
   NOOP,
-  hasOwn,
   isPromise
 } from '@vue/shared'
 import { computed } from './apiComputed'
@@ -47,7 +46,6 @@ import {
   reactive,
   ComputedGetter,
   WritableComputedOptions,
-  toRaw,
   proxyRefs,
   toRef
 } from '@vue/reactivity'
@@ -73,12 +71,6 @@ import {
   isCompatEnabled,
   softAssertCompatEnabled
 } from './compat/compatConfig'
-import {
-  AssetTypes,
-  COMPONENTS,
-  DIRECTIVES,
-  FILTERS
-} from './helpers/resolveAssets'
 import { OptionMergeFunction } from './apiCreateApp'
 
 /**
@@ -389,12 +381,12 @@ type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
 
 type ComponentWatchOptions = Record<string, ComponentWatchOptionItem>
 
-type ComponentInjectOptions =
-  | string[]
-  | Record<
-      string | symbol,
-      string | symbol | { from?: string | symbol; default?: unknown }
-    >
+type ComponentInjectOptions = string[] | ObjectInjectOptions
+
+type ObjectInjectOptions = Record<
+  string | symbol,
+  string | symbol | { from?: string | symbol; default?: unknown }
+>
 
 interface LegacyOptions<
   Props,
@@ -484,6 +476,9 @@ interface LegacyOptions<
 
 type MergedHook<T = (() => void)> = T | T[]
 
+export type MergedComponentOptions = ComponentOptions &
+  MergedComponentOptionsOverride
+
 export type MergedComponentOptionsOverride = {
   beforeCreate?: MergedHook
   created?: MergedHook
@@ -541,26 +536,23 @@ function createDuplicateChecker() {
   }
 }
 
-type DataFn = (vm: ComponentPublicInstance) => any
-
 export let shouldCacheAccess = true
 
-export function applyOptions(
-  instance: ComponentInternalInstance,
-  options: ComponentOptions,
-  deferredData: DataFn[] = [],
-  deferredWatch: ComponentWatchOptions[] = [],
-  deferredProvide: (Data | Function)[] = [],
-  asMixin: boolean = false
-) {
-  if (__COMPAT__ && isFunction(options)) {
-    options = options.options
+export function applyOptions(instance: ComponentInternalInstance) {
+  const options = resolveMergedOptions(instance)
+  const publicThis = instance.proxy!
+  const ctx = instance.ctx
+
+  // do not cache property access on public proxy during state initialization
+  shouldCacheAccess = false
+
+  // call beforeCreate first before accessing other options since
+  // the hook may mutate resolved options (#2791)
+  if (options.beforeCreate) {
+    callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
   }
 
   const {
-    // composition
-    mixins,
-    extends: extendsOptions,
     // state
     data: dataOptions,
     computed: computedOptions,
@@ -569,6 +561,7 @@ export function applyOptions(
     provide: provideOptions,
     inject: injectOptions,
     // lifecycle
+    created,
     beforeMount,
     mounted,
     beforeUpdate,
@@ -586,50 +579,13 @@ export function applyOptions(
     serverPrefetch,
     // public API
     expose,
-    inheritAttrs
+    inheritAttrs,
+    // assets
+    components,
+    directives,
+    filters
   } = options
 
-  const publicThis = instance.proxy!
-  const ctx = instance.ctx
-  const globalMixins = instance.appContext.mixins
-
-  // applyOptions is called non-as-mixin once per instance
-  if (!asMixin) {
-    shouldCacheAccess = false
-    callSyncHook(
-      'beforeCreate',
-      LifecycleHooks.BEFORE_CREATE,
-      options,
-      instance,
-      globalMixins
-    )
-    shouldCacheAccess = true
-    // global mixins are applied first
-    applyMixins(
-      instance,
-      globalMixins,
-      deferredData,
-      deferredWatch,
-      deferredProvide
-    )
-  }
-
-  // extending a base component...
-  if (extendsOptions) {
-    applyOptions(
-      instance,
-      extendsOptions,
-      deferredData,
-      deferredWatch,
-      deferredProvide,
-      true
-    )
-  }
-  // local mixins
-  if (mixins) {
-    applyMixins(instance, mixins, deferredData, deferredWatch, deferredProvide)
-  }
-
   const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
 
   if (__DEV__) {
@@ -681,33 +637,45 @@ export function applyOptions(
     }
   }
 
-  if (!asMixin) {
-    if (deferredData.length) {
-      deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis))
+  if (dataOptions) {
+    if (__DEV__ && !isFunction(dataOptions)) {
+      warn(
+        `The data option must be a function. ` +
+          `Plain object usage is no longer supported.`
+      )
     }
-    if (dataOptions) {
-      // @ts-ignore dataOptions is not fully type safe
-      resolveData(instance, dataOptions, publicThis)
+    const data = (dataOptions as any).call(publicThis, publicThis)
+    if (__DEV__ && isPromise(data)) {
+      warn(
+        `data() returned a Promise - note data() cannot be async; If you ` +
+          `intend to perform data fetching before component renders, use ` +
+          `async setup() + <Suspense>.`
+      )
     }
-    if (__DEV__) {
-      const rawData = toRaw(instance.data)
-      for (const key in rawData) {
-        checkDuplicateProperties!(OptionTypes.DATA, key)
-        // expose data on ctx during dev
-        if (key[0] !== '$' && key[0] !== '_') {
-          Object.defineProperty(ctx, key, {
-            configurable: true,
-            enumerable: true,
-            get: () => rawData[key],
-            set: NOOP
-          })
+    if (!isObject(data)) {
+      __DEV__ && warn(`data() should return an object.`)
+    } else {
+      instance.data = reactive(data)
+      if (__DEV__) {
+        for (const key in data) {
+          checkDuplicateProperties!(OptionTypes.DATA, key)
+          // expose data on ctx during dev
+          if (key[0] !== '$' && key[0] !== '_') {
+            Object.defineProperty(ctx, key, {
+              configurable: true,
+              enumerable: true,
+              get: () => data[key],
+              set: NOOP
+            })
+          }
         }
       }
     }
-  } else if (dataOptions) {
-    deferredData.push(dataOptions as DataFn)
   }
 
+  // state initialization complete at this point - start caching access
+  shouldCacheAccess = true
+
   if (computedOptions) {
     for (const key in computedOptions) {
       const opt = (computedOptions as ComputedOptions)[key]
@@ -746,47 +714,29 @@ export function applyOptions(
   }
 
   if (watchOptions) {
-    deferredWatch.push(watchOptions)
-  }
-  if (!asMixin && deferredWatch.length) {
-    deferredWatch.forEach(watchOptions => {
-      for (const key in watchOptions) {
-        createWatcher(watchOptions[key], ctx, publicThis, key)
-      }
-    })
+    for (const key in watchOptions) {
+      createWatcher(watchOptions[key], ctx, publicThis, key)
+    }
   }
 
   if (provideOptions) {
-    deferredProvide.push(provideOptions)
-  }
-  if (!asMixin && deferredProvide.length) {
-    deferredProvide.forEach(provideOptions => {
-      const provides = isFunction(provideOptions)
-        ? provideOptions.call(publicThis)
-        : provideOptions
-      Reflect.ownKeys(provides).forEach(key => {
-        provide(key, provides[key])
-      })
+    const provides = isFunction(provideOptions)
+      ? provideOptions.call(publicThis)
+      : provideOptions
+    Reflect.ownKeys(provides).forEach(key => {
+      provide(key, provides[key])
     })
   }
 
-  // lifecycle options
-  if (!asMixin) {
-    callSyncHook(
-      'created',
-      LifecycleHooks.CREATED,
-      options,
-      instance,
-      globalMixins
-    )
+  if (created) {
+    callHook(created, instance, LifecycleHooks.CREATED)
   }
 
   function registerLifecycleHook(
     register: Function,
     hook?: Function | Function[]
   ) {
-    // Array lifecycle hooks are only present in the compat build
-    if (__COMPAT__ && isArray(hook)) {
+    if (isArray(hook)) {
       hook.forEach(_hook => register(_hook.bind(publicThis)))
     } else if (hook) {
       register((hook as Function).bind(publicThis))
@@ -822,56 +772,34 @@ export function applyOptions(
   }
 
   if (isArray(expose)) {
-    if (!asMixin) {
-      if (expose.length) {
-        const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
-        expose.forEach(key => {
-          exposed[key] = toRef(publicThis, key as any)
-        })
-      } else if (!instance.exposed) {
-        instance.exposed = EMPTY_OBJ
-      }
-    } else if (__DEV__) {
-      warn(`The \`expose\` option is ignored when used in mixins.`)
+    if (expose.length) {
+      const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
+      expose.forEach(key => {
+        exposed[key] = toRef(publicThis, key as any)
+      })
+    } else if (!instance.exposed) {
+      instance.exposed = EMPTY_OBJ
     }
   }
 
   // options that are handled when creating the instance but also need to be
   // applied from mixins
-  if (asMixin) {
-    if (render && instance.render === NOOP) {
-      instance.render = render as InternalRenderFunction
-    }
-
-    if (inheritAttrs != null && instance.type.inheritAttrs == null) {
-      instance.inheritAttrs = inheritAttrs
-    }
-
-    // asset options.
-    // To reduce memory usage, only components with mixins or extends will have
-    // resolved asset registry attached to instance.
-    resolveInstanceAssets(instance, options, COMPONENTS)
-    resolveInstanceAssets(instance, options, DIRECTIVES)
-    if (__COMPAT__ && isCompatEnabled(DeprecationTypes.FILTERS, instance)) {
-      resolveInstanceAssets(instance, options, FILTERS)
-    }
+  if (render && instance.render === NOOP) {
+    instance.render = render as InternalRenderFunction
+  }
+  if (inheritAttrs != null) {
+    instance.inheritAttrs = inheritAttrs
   }
-}
 
-function resolveInstanceAssets(
-  instance: ComponentInternalInstance,
-  mixin: ComponentOptions,
-  type: AssetTypes
-) {
-  if (mixin[type]) {
-    extend(
-      instance[type] ||
-        (instance[type] = extend(
-          {},
-          (instance.type as ComponentOptions)[type]
-        ) as any),
-      mixin[type]
-    )
+  // asset options.
+  if (components) instance.components = components as any
+  if (directives) instance.directives = directives
+  if (
+    __COMPAT__ &&
+    filters &&
+    isCompatEnabled(DeprecationTypes.FILTERS, instance)
+  ) {
+    instance.filters = filters
   }
 }
 
@@ -881,127 +809,41 @@ export function resolveInjections(
   checkDuplicateProperties = NOOP as any
 ) {
   if (isArray(injectOptions)) {
-    for (let i = 0; i < injectOptions.length; i++) {
-      const key = injectOptions[i]
-      ctx[key] = inject(key)
-      if (__DEV__) {
-        checkDuplicateProperties!(OptionTypes.INJECT, key)
-      }
-    }
-  } else {
-    for (const key in injectOptions) {
-      const opt = injectOptions[key]
-      if (isObject(opt)) {
+    injectOptions = normalizeInject(injectOptions)!
+  }
+  for (const key in injectOptions) {
+    const opt = (injectOptions as ObjectInjectOptions)[key]
+    if (isObject(opt)) {
+      if ('default' in opt) {
         ctx[key] = inject(
           opt.from || key,
           opt.default,
           true /* treat default function as factory */
         )
       } else {
-        ctx[key] = inject(opt)
-      }
-      if (__DEV__) {
-        checkDuplicateProperties!(OptionTypes.INJECT, key)
+        ctx[key] = inject(opt.from || key)
       }
+    } else {
+      ctx[key] = inject(opt)
     }
-  }
-}
-
-function callSyncHook(
-  name: 'beforeCreate' | 'created',
-  type: LifecycleHooks,
-  options: ComponentOptions,
-  instance: ComponentInternalInstance,
-  globalMixins: ComponentOptions[]
-) {
-  for (let i = 0; i < globalMixins.length; i++) {
-    callHookWithMixinAndExtends(name, type, globalMixins[i], instance)
-  }
-  callHookWithMixinAndExtends(name, type, options, instance)
-}
-
-function callHookWithMixinAndExtends(
-  name: 'beforeCreate' | 'created',
-  type: LifecycleHooks,
-  options: ComponentOptions,
-  instance: ComponentInternalInstance
-) {
-  const { extends: base, mixins } = options
-  const selfHook = options[name]
-  if (base) {
-    callHookWithMixinAndExtends(name, type, base, instance)
-  }
-  if (mixins) {
-    for (let i = 0; i < mixins.length; i++) {
-      callHookWithMixinAndExtends(name, type, mixins[i], instance)
+    if (__DEV__) {
+      checkDuplicateProperties!(OptionTypes.INJECT, key)
     }
   }
-  if (selfHook) {
-    callWithAsyncErrorHandling(
-      __COMPAT__ && isArray(selfHook)
-        ? selfHook.map(h => h.bind(instance.proxy!))
-        : selfHook.bind(instance.proxy!),
-      instance,
-      type
-    )
-  }
 }
 
-function applyMixins(
+function callHook(
+  hook: Function,
   instance: ComponentInternalInstance,
-  mixins: ComponentOptions[],
-  deferredData: DataFn[],
-  deferredWatch: ComponentWatchOptions[],
-  deferredProvide: (Data | Function)[]
+  type: LifecycleHooks
 ) {
-  for (let i = 0; i < mixins.length; i++) {
-    applyOptions(
-      instance,
-      mixins[i],
-      deferredData,
-      deferredWatch,
-      deferredProvide,
-      true
-    )
-  }
-}
-
-function resolveData(
-  instance: ComponentInternalInstance,
-  dataFn: DataFn,
-  publicThis: ComponentPublicInstance
-) {
-  if (__DEV__ && !isFunction(dataFn)) {
-    warn(
-      `The data option must be a function. ` +
-        `Plain object usage is no longer supported.`
-    )
-  }
-  shouldCacheAccess = false
-  const data = dataFn.call(publicThis, publicThis)
-  shouldCacheAccess = true
-  if (__DEV__ && isPromise(data)) {
-    warn(
-      `data() returned a Promise - note data() cannot be async; If you ` +
-        `intend to perform data fetching before component renders, use ` +
-        `async setup() + <Suspense>.`
-    )
-  }
-  if (!isObject(data)) {
-    __DEV__ && warn(`data() should return an object.`)
-  } else if (instance.data === EMPTY_OBJ) {
-    instance.data = reactive(data)
-  } else {
-    // existing data: this is a mixin or extends.
-    if (
-      __COMPAT__ &&
-      isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, instance)
-    ) {
-      deepMergeData(instance.data, data, instance)
-    } else {
-      extend(instance.data, data)
-    }
-  }
+  callWithAsyncErrorHandling(
+    isArray(hook)
+      ? hook.map(h => h.bind(instance.proxy!))
+      : hook.bind(instance.proxy!),
+    instance,
+    type
+  )
 }
 
 export function createWatcher(
@@ -1047,7 +889,7 @@ export function createWatcher(
  */
 export function resolveMergedOptions(
   instance: ComponentInternalInstance
-): ComponentOptions & MergedComponentOptionsOverride {
+): MergedComponentOptions {
   const base = instance.type as ComponentOptions
   const { mixins, extends: extendsOptions } = base
   const {
@@ -1057,7 +899,7 @@ export function resolveMergedOptions(
   } = instance.appContext
   const cached = cache.get(base)
 
-  let resolved: ComponentOptions
+  let resolved: MergedComponentOptions
 
   if (cached) {
     resolved = cached
@@ -1066,17 +908,17 @@ export function resolveMergedOptions(
       __COMPAT__ &&
       isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance)
     ) {
-      resolved = extend({}, base)
+      resolved = extend({}, base) as MergedComponentOptions
       resolved.parent = instance.parent && instance.parent.proxy
       resolved.propsData = instance.vnode.props
     } else {
-      resolved = base
+      resolved = base as MergedComponentOptions
     }
   } else {
     resolved = {}
     if (globalMixins.length) {
       globalMixins.forEach(m =>
-        mergeOptions(resolved, m, optionMergeStrategies)
+        mergeOptions(resolved, m, optionMergeStrategies, true)
       )
     }
     mergeOptions(resolved, base, optionMergeStrategies)
@@ -1089,7 +931,8 @@ export function resolveMergedOptions(
 export function mergeOptions(
   to: any,
   from: any,
-  strats: Record<string, OptionMergeFunction>
+  strats: Record<string, OptionMergeFunction>,
+  asMixin = false
 ) {
   if (__COMPAT__ && isFunction(from)) {
     from = from.options
@@ -1098,18 +941,110 @@ export function mergeOptions(
   const { mixins, extends: extendsOptions } = from
 
   if (extendsOptions) {
-    mergeOptions(to, extendsOptions, strats)
+    mergeOptions(to, extendsOptions, strats, true)
   }
   if (mixins) {
-    mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, strats))
+    mixins.forEach((m: ComponentOptionsMixin) =>
+      mergeOptions(to, m, strats, true)
+    )
   }
 
   for (const key in from) {
-    if (strats && hasOwn(strats, key)) {
-      to[key] = strats[key](to[key], from[key])
+    if (asMixin && key === 'expose') {
+      __DEV__ &&
+        warn(
+          `"expose" option is ignored when declared in mixins or extends. ` +
+            `It should only be declared in the base component itself.`
+        )
     } else {
-      to[key] = from[key]
+      const strat = internalOptionMergeStrats[key] || (strats && strats[key])
+      to[key] = strat ? strat(to[key], from[key]) : from[key]
     }
   }
   return to
 }
+
+export const internalOptionMergeStrats: Record<string, Function> = {
+  data: mergeDataFn,
+  props: mergeObjectOptions, // TODO
+  emits: mergeObjectOptions, // TODO
+  // objects
+  methods: mergeObjectOptions,
+  computed: mergeObjectOptions,
+  // lifecycle
+  beforeCreate: mergeHook,
+  created: mergeHook,
+  beforeMount: mergeHook,
+  mounted: mergeHook,
+  beforeUpdate: mergeHook,
+  updated: mergeHook,
+  beforeDestroy: mergeHook,
+  destroyed: mergeHook,
+  activated: mergeHook,
+  deactivated: mergeHook,
+  errorCaptured: mergeHook,
+  serverPrefetch: mergeHook,
+  // assets
+  components: mergeObjectOptions,
+  directives: mergeObjectOptions,
+  // watch has special merge behavior in v2, but isn't actually needed in v3.
+  // since we are only exposing these for compat and nobody should be relying
+  // on the watch-specific behavior, just expose the object merge strat.
+  watch: mergeObjectOptions,
+  // provide / inject
+  provide: mergeDataFn,
+  inject: mergeInject
+}
+
+if (__COMPAT__) {
+  internalOptionMergeStrats.filters = mergeObjectOptions
+}
+
+function mergeDataFn(to: any, from: any) {
+  if (!from) {
+    return to
+  }
+  if (!to) {
+    return from
+  }
+  return function mergedDataFn(this: ComponentPublicInstance) {
+    return (__COMPAT__ &&
+    isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, null)
+      ? deepMergeData
+      : extend)(
+      isFunction(to) ? to.call(this, this) : to,
+      isFunction(from) ? from.call(this, this) : from
+    )
+  }
+}
+
+function mergeInject(
+  to: ComponentInjectOptions | undefined,
+  from: ComponentInjectOptions
+) {
+  return mergeObjectOptions(normalizeInject(to), normalizeInject(from))
+}
+
+function normalizeInject(
+  raw: ComponentInjectOptions | undefined
+): ObjectInjectOptions | undefined {
+  if (isArray(raw)) {
+    const res: ObjectInjectOptions = {}
+    for (let i = 0; i < raw.length; i++) {
+      res[raw[i]] = raw[i]
+    }
+    return res
+  }
+  return raw
+}
+
+function mergeHook(
+  to: Function[] | Function | undefined,
+  from: Function | Function[]
+) {
+  return to ? [...new Set([].concat(to as any, from as any))] : from
+}
+
+function mergeObjectOptions(to: Object | undefined, from: Object | undefined) {
+  return to ? extend(extend(Object.create(null), to), from) : from
+}
index 119bdbe6d4f78b00dbb45b382c952ce44fca8949..dc1db52ce24b207838934f471f64b328a52b5c60 100644 (file)
@@ -99,7 +99,7 @@ function resolveAsset(
 
     const res =
       // local registration
-      // check instance[type] first for components with mixin or extends.
+      // check instance[type] first which is resolved for options API
       resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
       // global registration
       resolve(instance.appContext[type], name)