fix(runtime-core): align option merge behavior with Vue 2
fix #3566, #2791
This commit is contained in:
parent
1e35a860b9
commit
e2ca67b59a
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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.$
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -795,7 +795,7 @@ export function finishComponentSetup(
|
||||
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
|
||||
currentInstance = instance
|
||||
pauseTracking()
|
||||
applyOptions(instance, Component)
|
||||
applyOptions(instance)
|
||||
resetTracking()
|
||||
currentInstance = null
|
||||
}
|
||||
|
@ -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,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() + <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)
|
||||
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<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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user