fix(runtime-core): align option merge behavior with Vue 2

fix #3566, #2791
This commit is contained in:
Evan You 2021-06-02 14:37:27 -04:00
parent 1e35a860b9
commit e2ca67b59a
9 changed files with 439 additions and 375 deletions

View 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', () => { describe('warnings', () => {
test('Expected a function as watch handler', () => { test('Expected a function as watch handler', () => {
const Comp = { const Comp = {

View File

@ -4,7 +4,11 @@ import {
validateComponentName, validateComponentName,
Component Component
} from './component' } from './component'
import { ComponentOptions, RuntimeCompilerOptions } from './componentOptions' import {
ComponentOptions,
MergedComponentOptions,
RuntimeCompilerOptions
} from './componentOptions'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives' import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer' import { RootRenderFunction } from './renderer'
@ -98,7 +102,7 @@ export interface AppContext {
* Each app instance has its own cache because app-level global mixins and * Each app instance has its own cache because app-level global mixins and
* optionMergeStrategies can affect merge behavior. * optionMergeStrategies can affect merge behavior.
*/ */
cache: WeakMap<ComponentOptions, ComponentOptions> cache: WeakMap<ComponentOptions, MergedComponentOptions>
/** /**
* Flag for de-optimizing props normalization * Flag for de-optimizing props normalization
* @internal * @internal

View File

@ -531,7 +531,10 @@ const seenConfigObjects = /*#__PURE__*/ new WeakSet<CompatConfig>()
const warnedInvalidKeys: Record<string, boolean> = {} const warnedInvalidKeys: Record<string, boolean> = {}
// dev only // dev only
export function validateCompatConfig(config: CompatConfig) { export function validateCompatConfig(
config: CompatConfig,
instance?: ComponentInternalInstance
) {
if (seenConfigObjects.has(config)) { if (seenConfigObjects.has(config)) {
return return
} }
@ -558,6 +561,14 @@ export function validateCompatConfig(config: CompatConfig) {
warnedInvalidKeys[key] = true 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( export function getCompatConfigForKey(

View File

@ -1,39 +1,16 @@
import { isFunction, isPlainObject } from '@vue/shared' import { isPlainObject } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './compatConfig' import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function deepMergeData( export function deepMergeData(to: any, from: any) {
to: any,
from: any,
instance: ComponentInternalInstance
) {
for (const key in from) { for (const key in from) {
const toVal = to[key] const toVal = to[key]
const fromVal = from[key] const fromVal = from[key]
if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) { if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) {
__DEV__ && __DEV__ && warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, null, key)
warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, instance, key) deepMergeData(toVal, fromVal)
deepMergeData(toVal, fromVal, instance)
} else { } else {
to[key] = fromVal to[key] = fromVal
} }
} }
return to 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.$
)
}
}

View File

@ -34,7 +34,11 @@ import {
isRuntimeOnly, isRuntimeOnly,
setupComponent setupComponent
} from '../component' } from '../component'
import { RenderFunction, mergeOptions } from '../componentOptions' import {
RenderFunction,
mergeOptions,
internalOptionMergeStrats
} from '../componentOptions'
import { ComponentPublicInstance } from '../componentPublicInstance' import { ComponentPublicInstance } from '../componentPublicInstance'
import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools' import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools'
import { Directive } from '../directives' import { Directive } from '../directives'
@ -43,8 +47,7 @@ import { version } from '..'
import { import {
installLegacyConfigWarnings, installLegacyConfigWarnings,
installLegacyOptionMergeStrats, installLegacyOptionMergeStrats,
LegacyConfig, LegacyConfig
legacyOptionMergeStrats
} from './globalConfig' } from './globalConfig'
import { LegacyDirective } from './customDirective' import { LegacyDirective } from './customDirective'
import { import {
@ -231,8 +234,7 @@ export function createCompatVue(
mergeOptions( mergeOptions(
extend({}, SubVue.options), extend({}, SubVue.options),
inlineOptions, inlineOptions,
null, internalOptionMergeStrats as any
legacyOptionMergeStrats as any
), ),
SubVue SubVue
) )
@ -257,8 +259,7 @@ export function createCompatVue(
SubVue.options = mergeOptions( SubVue.options = mergeOptions(
mergeBase, mergeBase,
extendOptions, extendOptions,
null, internalOptionMergeStrats as any
legacyOptionMergeStrats as any
) )
SubVue.options._base = SubVue SubVue.options._base = SubVue
@ -305,8 +306,7 @@ export function createCompatVue(
mergeOptions( mergeOptions(
parent, parent,
child, child,
vm && vm.$, vm ? undefined : (internalOptionMergeStrats as any)
vm ? undefined : (legacyOptionMergeStrats as any)
), ),
defineReactive defineReactive
} }

View File

@ -1,12 +1,11 @@
import { extend, isArray } from '@vue/shared'
import { AppConfig } from '../apiCreateApp' import { AppConfig } from '../apiCreateApp'
import { mergeDataOption } from './data'
import { import {
DeprecationTypes, DeprecationTypes,
softAssertCompatEnabled, softAssertCompatEnabled,
warnDeprecation warnDeprecation
} from './compatConfig' } from './compatConfig'
import { isCopyingConfig } from './global' import { isCopyingConfig } from './global'
import { internalOptionMergeStrats } from '../componentOptions'
// legacy config warnings // legacy config warnings
export type LegacyConfig = { export type LegacyConfig = {
@ -70,60 +69,16 @@ export function installLegacyOptionMergeStrats(config: AppConfig) {
return target[key] return target[key]
} }
if ( if (
key in legacyOptionMergeStrats && key in internalOptionMergeStrats &&
softAssertCompatEnabled( softAssertCompatEnabled(
DeprecationTypes.CONFIG_OPTION_MERGE_STRATS, DeprecationTypes.CONFIG_OPTION_MERGE_STRATS,
null null
) )
) { ) {
return legacyOptionMergeStrats[ return internalOptionMergeStrats[
key as keyof typeof legacyOptionMergeStrats 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
}

View File

@ -795,7 +795,7 @@ export function finishComponentSetup(
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
currentInstance = instance currentInstance = instance
pauseTracking() pauseTracking()
applyOptions(instance, Component) applyOptions(instance)
resetTracking() resetTracking()
currentInstance = null currentInstance = null
} }

View File

@ -16,7 +16,6 @@ import {
isArray, isArray,
EMPTY_OBJ, EMPTY_OBJ,
NOOP, NOOP,
hasOwn,
isPromise isPromise
} from '@vue/shared' } from '@vue/shared'
import { computed } from './apiComputed' import { computed } from './apiComputed'
@ -47,7 +46,6 @@ import {
reactive, reactive,
ComputedGetter, ComputedGetter,
WritableComputedOptions, WritableComputedOptions,
toRaw,
proxyRefs, proxyRefs,
toRef toRef
} from '@vue/reactivity' } from '@vue/reactivity'
@ -73,12 +71,6 @@ import {
isCompatEnabled, isCompatEnabled,
softAssertCompatEnabled softAssertCompatEnabled
} from './compat/compatConfig' } from './compat/compatConfig'
import {
AssetTypes,
COMPONENTS,
DIRECTIVES,
FILTERS
} from './helpers/resolveAssets'
import { OptionMergeFunction } from './apiCreateApp' import { OptionMergeFunction } from './apiCreateApp'
/** /**
@ -389,12 +381,12 @@ type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
type ComponentWatchOptions = Record<string, ComponentWatchOptionItem> type ComponentWatchOptions = Record<string, ComponentWatchOptionItem>
type ComponentInjectOptions = type ComponentInjectOptions = string[] | ObjectInjectOptions
| string[]
| Record< type ObjectInjectOptions = Record<
string | symbol, string | symbol,
string | symbol | { from?: string | symbol; default?: unknown } string | symbol | { from?: string | symbol; default?: unknown }
> >
interface LegacyOptions< interface LegacyOptions<
Props, Props,
@ -484,6 +476,9 @@ interface LegacyOptions<
type MergedHook<T = (() => void)> = T | T[] type MergedHook<T = (() => void)> = T | T[]
export type MergedComponentOptions = ComponentOptions &
MergedComponentOptionsOverride
export type MergedComponentOptionsOverride = { export type MergedComponentOptionsOverride = {
beforeCreate?: MergedHook beforeCreate?: MergedHook
created?: MergedHook created?: MergedHook
@ -541,26 +536,23 @@ function createDuplicateChecker() {
} }
} }
type DataFn = (vm: ComponentPublicInstance) => any
export let shouldCacheAccess = true export let shouldCacheAccess = true
export function applyOptions( export function applyOptions(instance: ComponentInternalInstance) {
instance: ComponentInternalInstance, const options = resolveMergedOptions(instance)
options: ComponentOptions, const publicThis = instance.proxy!
deferredData: DataFn[] = [], const ctx = instance.ctx
deferredWatch: ComponentWatchOptions[] = [],
deferredProvide: (Data | Function)[] = [], // do not cache property access on public proxy during state initialization
asMixin: boolean = false shouldCacheAccess = false
) {
if (__COMPAT__ && isFunction(options)) { // call beforeCreate first before accessing other options since
options = options.options // the hook may mutate resolved options (#2791)
if (options.beforeCreate) {
callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
} }
const { const {
// composition
mixins,
extends: extendsOptions,
// state // state
data: dataOptions, data: dataOptions,
computed: computedOptions, computed: computedOptions,
@ -569,6 +561,7 @@ export function applyOptions(
provide: provideOptions, provide: provideOptions,
inject: injectOptions, inject: injectOptions,
// lifecycle // lifecycle
created,
beforeMount, beforeMount,
mounted, mounted,
beforeUpdate, beforeUpdate,
@ -586,50 +579,13 @@ export function applyOptions(
serverPrefetch, serverPrefetch,
// public API // public API
expose, expose,
inheritAttrs inheritAttrs,
// assets
components,
directives,
filters
} = options } = 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 const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
if (__DEV__) { if (__DEV__) {
@ -681,33 +637,45 @@ export function applyOptions(
} }
} }
if (!asMixin) { if (dataOptions) {
if (deferredData.length) { if (__DEV__ && !isFunction(dataOptions)) {
deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis)) warn(
`The data option must be a function. ` +
`Plain object usage is no longer supported.`
)
} }
if (dataOptions) { const data = (dataOptions as any).call(publicThis, publicThis)
// @ts-ignore dataOptions is not fully type safe if (__DEV__ && isPromise(data)) {
resolveData(instance, dataOptions, publicThis) 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__) { if (!isObject(data)) {
const rawData = toRaw(instance.data) __DEV__ && warn(`data() should return an object.`)
for (const key in rawData) { } else {
checkDuplicateProperties!(OptionTypes.DATA, key) instance.data = reactive(data)
// expose data on ctx during dev if (__DEV__) {
if (key[0] !== '$' && key[0] !== '_') { for (const key in data) {
Object.defineProperty(ctx, key, { checkDuplicateProperties!(OptionTypes.DATA, key)
configurable: true, // expose data on ctx during dev
enumerable: true, if (key[0] !== '$' && key[0] !== '_') {
get: () => rawData[key], Object.defineProperty(ctx, key, {
set: NOOP 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) { if (computedOptions) {
for (const key in computedOptions) { for (const key in computedOptions) {
const opt = (computedOptions as ComputedOptions)[key] const opt = (computedOptions as ComputedOptions)[key]
@ -746,47 +714,29 @@ export function applyOptions(
} }
if (watchOptions) { if (watchOptions) {
deferredWatch.push(watchOptions) for (const key in watchOptions) {
} createWatcher(watchOptions[key], ctx, publicThis, key)
if (!asMixin && deferredWatch.length) { }
deferredWatch.forEach(watchOptions => {
for (const key in watchOptions) {
createWatcher(watchOptions[key], ctx, publicThis, key)
}
})
} }
if (provideOptions) { if (provideOptions) {
deferredProvide.push(provideOptions) const provides = isFunction(provideOptions)
} ? provideOptions.call(publicThis)
if (!asMixin && deferredProvide.length) { : provideOptions
deferredProvide.forEach(provideOptions => { Reflect.ownKeys(provides).forEach(key => {
const provides = isFunction(provideOptions) provide(key, provides[key])
? provideOptions.call(publicThis)
: provideOptions
Reflect.ownKeys(provides).forEach(key => {
provide(key, provides[key])
})
}) })
} }
// lifecycle options if (created) {
if (!asMixin) { callHook(created, instance, LifecycleHooks.CREATED)
callSyncHook(
'created',
LifecycleHooks.CREATED,
options,
instance,
globalMixins
)
} }
function registerLifecycleHook( function registerLifecycleHook(
register: Function, register: Function,
hook?: Function | Function[] hook?: Function | Function[]
) { ) {
// Array lifecycle hooks are only present in the compat build if (isArray(hook)) {
if (__COMPAT__ && isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis))) hook.forEach(_hook => register(_hook.bind(publicThis)))
} else if (hook) { } else if (hook) {
register((hook as Function).bind(publicThis)) register((hook as Function).bind(publicThis))
@ -822,56 +772,34 @@ export function applyOptions(
} }
if (isArray(expose)) { if (isArray(expose)) {
if (!asMixin) { if (expose.length) {
if (expose.length) { const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
const exposed = instance.exposed || (instance.exposed = proxyRefs({})) expose.forEach(key => {
expose.forEach(key => { exposed[key] = toRef(publicThis, key as any)
exposed[key] = toRef(publicThis, key as any) })
}) } else if (!instance.exposed) {
} else if (!instance.exposed) { instance.exposed = EMPTY_OBJ
instance.exposed = EMPTY_OBJ
}
} else if (__DEV__) {
warn(`The \`expose\` option is ignored when used in mixins.`)
} }
} }
// options that are handled when creating the instance but also need to be // options that are handled when creating the instance but also need to be
// applied from mixins // applied from mixins
if (asMixin) { if (render && instance.render === NOOP) {
if (render && instance.render === NOOP) { instance.render = render as InternalRenderFunction
instance.render = render as InternalRenderFunction }
} if (inheritAttrs != null) {
instance.inheritAttrs = inheritAttrs
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)
}
} }
}
function resolveInstanceAssets( // asset options.
instance: ComponentInternalInstance, if (components) instance.components = components as any
mixin: ComponentOptions, if (directives) instance.directives = directives
type: AssetTypes if (
) { __COMPAT__ &&
if (mixin[type]) { filters &&
extend( isCompatEnabled(DeprecationTypes.FILTERS, instance)
instance[type] || ) {
(instance[type] = extend( instance.filters = filters
{},
(instance.type as ComponentOptions)[type]
) as any),
mixin[type]
)
} }
} }
@ -881,129 +809,43 @@ export function resolveInjections(
checkDuplicateProperties = NOOP as any checkDuplicateProperties = NOOP as any
) { ) {
if (isArray(injectOptions)) { if (isArray(injectOptions)) {
for (let i = 0; i < injectOptions.length; i++) { injectOptions = normalizeInject(injectOptions)!
const key = injectOptions[i] }
ctx[key] = inject(key) for (const key in injectOptions) {
if (__DEV__) { const opt = (injectOptions as ObjectInjectOptions)[key]
checkDuplicateProperties!(OptionTypes.INJECT, key) if (isObject(opt)) {
} if ('default' in opt) {
}
} else {
for (const key in injectOptions) {
const opt = injectOptions[key]
if (isObject(opt)) {
ctx[key] = inject( ctx[key] = inject(
opt.from || key, opt.from || key,
opt.default, opt.default,
true /* treat default function as factory */ true /* treat default function as factory */
) )
} else { } 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 { } 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( export function createWatcher(
raw: ComponentWatchOptionItem, raw: ComponentWatchOptionItem,
ctx: Data, ctx: Data,
@ -1047,7 +889,7 @@ export function createWatcher(
*/ */
export function resolveMergedOptions( export function resolveMergedOptions(
instance: ComponentInternalInstance instance: ComponentInternalInstance
): ComponentOptions & MergedComponentOptionsOverride { ): MergedComponentOptions {
const base = instance.type as ComponentOptions const base = instance.type as ComponentOptions
const { mixins, extends: extendsOptions } = base const { mixins, extends: extendsOptions } = base
const { const {
@ -1057,7 +899,7 @@ export function resolveMergedOptions(
} = instance.appContext } = instance.appContext
const cached = cache.get(base) const cached = cache.get(base)
let resolved: ComponentOptions let resolved: MergedComponentOptions
if (cached) { if (cached) {
resolved = cached resolved = cached
@ -1066,17 +908,17 @@ export function resolveMergedOptions(
__COMPAT__ && __COMPAT__ &&
isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance) isCompatEnabled(DeprecationTypes.PRIVATE_APIS, instance)
) { ) {
resolved = extend({}, base) resolved = extend({}, base) as MergedComponentOptions
resolved.parent = instance.parent && instance.parent.proxy resolved.parent = instance.parent && instance.parent.proxy
resolved.propsData = instance.vnode.props resolved.propsData = instance.vnode.props
} else { } else {
resolved = base resolved = base as MergedComponentOptions
} }
} else { } else {
resolved = {} resolved = {}
if (globalMixins.length) { if (globalMixins.length) {
globalMixins.forEach(m => globalMixins.forEach(m =>
mergeOptions(resolved, m, optionMergeStrategies) mergeOptions(resolved, m, optionMergeStrategies, true)
) )
} }
mergeOptions(resolved, base, optionMergeStrategies) mergeOptions(resolved, base, optionMergeStrategies)
@ -1089,7 +931,8 @@ export function resolveMergedOptions(
export function mergeOptions( export function mergeOptions(
to: any, to: any,
from: any, from: any,
strats: Record<string, OptionMergeFunction> strats: Record<string, OptionMergeFunction>,
asMixin = false
) { ) {
if (__COMPAT__ && isFunction(from)) { if (__COMPAT__ && isFunction(from)) {
from = from.options from = from.options
@ -1098,18 +941,110 @@ export function mergeOptions(
const { mixins, extends: extendsOptions } = from const { mixins, extends: extendsOptions } = from
if (extendsOptions) { if (extendsOptions) {
mergeOptions(to, extendsOptions, strats) mergeOptions(to, extendsOptions, strats, true)
} }
if (mixins) { if (mixins) {
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, strats)) mixins.forEach((m: ComponentOptionsMixin) =>
mergeOptions(to, m, strats, true)
)
} }
for (const key in from) { for (const key in from) {
if (strats && hasOwn(strats, key)) { if (asMixin && key === 'expose') {
to[key] = strats[key](to[key], from[key]) __DEV__ &&
warn(
`"expose" option is ignored when declared in mixins or extends. ` +
`It should only be declared in the base component itself.`
)
} else { } else {
to[key] = from[key] const strat = internalOptionMergeStrats[key] || (strats && strats[key])
to[key] = strat ? strat(to[key], from[key]) : from[key]
} }
} }
return to 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
}

View File

@ -99,7 +99,7 @@ function resolveAsset(
const res = const res =
// local registration // 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) || resolve(instance[type] || (Component as ComponentOptions)[type], name) ||
// global registration // global registration
resolve(instance.appContext[type], name) resolve(instance.appContext[type], name)