diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index 9da4e7b0..87d57afc 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -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 = { diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 6e340559..617ddd20 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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 + cache: WeakMap /** * Flag for de-optimizing props normalization * @internal diff --git a/packages/runtime-core/src/compat/compatConfig.ts b/packages/runtime-core/src/compat/compatConfig.ts index 9ca7de5c..f87831ee 100644 --- a/packages/runtime-core/src/compat/compatConfig.ts +++ b/packages/runtime-core/src/compat/compatConfig.ts @@ -531,7 +531,10 @@ const seenConfigObjects = /*#__PURE__*/ new WeakSet() const warnedInvalidKeys: Record = {} // 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( diff --git a/packages/runtime-core/src/compat/data.ts b/packages/runtime-core/src/compat/data.ts index fa8b1085..72c7314e 100644 --- a/packages/runtime-core/src/compat/data.ts +++ b/packages/runtime-core/src/compat/data.ts @@ -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.$ - ) - } -} diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts index 3d518f44..7823cc6a 100644 --- a/packages/runtime-core/src/compat/global.ts +++ b/packages/runtime-core/src/compat/global.ts @@ -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 } diff --git a/packages/runtime-core/src/compat/globalConfig.ts b/packages/runtime-core/src/compat/globalConfig.ts index 593cd20e..51f22ace 100644 --- a/packages/runtime-core/src/compat/globalConfig.ts +++ b/packages/runtime-core/src/compat/globalConfig.ts @@ -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 -} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index ec1aa65b..fd7b5777 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -795,7 +795,7 @@ export function finishComponentSetup( if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { currentInstance = instance pauseTracking() - applyOptions(instance, Component) + applyOptions(instance) resetTracking() currentInstance = null } diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 51d9c83a..3983e8d9 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -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 -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 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() + .` + ) } - 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,129 +809,43 @@ 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) + ctx[key] = inject(opt.from || key) } - if (__DEV__) { - checkDuplicateProperties!(OptionTypes.INJECT, key) - } - } - } -} - -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 (selfHook) { - callWithAsyncErrorHandling( - __COMPAT__ && isArray(selfHook) - ? selfHook.map(h => h.bind(instance.proxy!)) - : selfHook.bind(instance.proxy!), - instance, - type - ) - } -} - -function applyMixins( - instance: ComponentInternalInstance, - mixins: ComponentOptions[], - deferredData: DataFn[], - deferredWatch: ComponentWatchOptions[], - deferredProvide: (Data | Function)[] -) { - 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() + .` - ) - } - 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) + ctx[key] = inject(opt) + } + if (__DEV__) { + checkDuplicateProperties!(OptionTypes.INJECT, key) } } } +function callHook( + hook: Function, + instance: ComponentInternalInstance, + type: LifecycleHooks +) { + callWithAsyncErrorHandling( + isArray(hook) + ? hook.map(h => h.bind(instance.proxy!)) + : hook.bind(instance.proxy!), + instance, + type + ) +} + export function createWatcher( raw: ComponentWatchOptionItem, ctx: Data, @@ -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 + strats: Record, + 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 = { + 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 +} diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index 119bdbe6..dc1db52c 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -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)