isArray,
EMPTY_OBJ,
NOOP,
- hasOwn,
isPromise
} from '@vue/shared'
import { computed } from './apiComputed'
reactive,
ComputedGetter,
WritableComputedOptions,
- toRaw,
proxyRefs,
toRef
} from '@vue/reactivity'
isCompatEnabled,
softAssertCompatEnabled
} from './compat/compatConfig'
-import {
- AssetTypes,
- COMPONENTS,
- DIRECTIVES,
- FILTERS
-} from './helpers/resolveAssets'
import { OptionMergeFunction } from './apiCreateApp'
/**
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,
type MergedHook<T = (() => void)> = T | T[]
+export type MergedComponentOptions = ComponentOptions &
+ MergedComponentOptionsOverride
+
export type MergedComponentOptionsOverride = {
beforeCreate?: MergedHook
created?: MergedHook
}
}
-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,
provide: provideOptions,
inject: injectOptions,
// lifecycle
+ created,
beforeMount,
mounted,
beforeUpdate,
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__) {
}
}
- 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]
}
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))
}
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
}
}
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(
*/
export function resolveMergedOptions(
instance: ComponentInternalInstance
-): ComponentOptions & MergedComponentOptionsOverride {
+): MergedComponentOptions {
const base = instance.type as ComponentOptions
const { mixins, extends: extendsOptions } = base
const {
} = instance.appContext
const cached = cache.get(base)
- let resolved: ComponentOptions
+ let resolved: MergedComponentOptions
if (cached) {
resolved = cached
__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)
export function mergeOptions(
to: any,
from: any,
- strats: Record<string, OptionMergeFunction>
+ strats: Record<string, OptionMergeFunction>,
+ asMixin = false
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
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
+}