Skip to content

Commit 528621b

Browse files
committed
feat(runtime-core): support config.optionMergeStrategies
Note the behavior is different from Vue 2: - merge strategies no longer apply to built-in options. - the default value is now an empty object and no longer exposes merge strategies for built-in options.
1 parent 1237387 commit 528621b

File tree

5 files changed

+82
-11
lines changed

5 files changed

+82
-11
lines changed

packages/runtime-core/__tests__/apiOptions.spec.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
nextTick,
99
renderToString,
1010
ref,
11-
defineComponent
11+
defineComponent,
12+
createApp
1213
} from '@vue/runtime-test'
1314
import { mockWarn } from '@vue/shared'
1415

@@ -562,6 +563,28 @@ describe('api: options', () => {
562563
expect(serializeInner(root)).toBe(`<div>1,1,3</div>`)
563564
})
564565

566+
test('optionMergeStrategies', () => {
567+
let merged: string
568+
const App = defineComponent({
569+
render() {},
570+
mixins: [{ foo: 'mixin' }],
571+
extends: { foo: 'extends' },
572+
foo: 'local',
573+
mounted() {
574+
merged = this.$options.foo
575+
}
576+
})
577+
578+
const app = createApp(App)
579+
app.mixin({
580+
foo: 'global'
581+
})
582+
app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
583+
584+
app.mount(nodeOps.createElement('div'))
585+
expect(merged!).toBe('global,extends,mixin,local')
586+
})
587+
565588
describe('warnings', () => {
566589
mockWarn()
567590

packages/runtime-core/src/apiCreateApp.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,21 @@ export interface App<HostElement = any> {
3636
_context: AppContext
3737
}
3838

39+
export type OptionMergeFunction = (
40+
to: unknown,
41+
from: unknown,
42+
instance: any,
43+
key: string
44+
) => any
45+
3946
export interface AppConfig {
47+
// @private
48+
readonly isNativeTag?: (tag: string) => boolean
49+
4050
devtools: boolean
4151
performance: boolean
42-
readonly isNativeTag?: (tag: string) => boolean
43-
isCustomElement?: (tag: string) => boolean
52+
optionMergeStrategies: Record<string, OptionMergeFunction>
53+
isCustomElement: (tag: string) => boolean
4454
errorHandler?: (
4555
err: unknown,
4656
instance: ComponentPublicInstance | null,
@@ -73,9 +83,10 @@ export type Plugin =
7383
export function createAppContext(): AppContext {
7484
return {
7585
config: {
86+
isNativeTag: NO,
7687
devtools: true,
7788
performance: false,
78-
isNativeTag: NO,
89+
optionMergeStrategies: {},
7990
isCustomElement: NO,
8091
errorHandler: undefined,
8192
warnHandler: undefined

packages/runtime-core/src/apiOptions.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
isObject,
1515
isArray,
1616
EMPTY_OBJ,
17-
NOOP
17+
NOOP,
18+
hasOwn
1819
} from '@vue/shared'
1920
import { computed } from './apiComputed'
2021
import { watch, WatchOptions, WatchCallback } from './apiWatch'
@@ -75,11 +76,16 @@ export interface ComponentOptionsBase<
7576
directives?: Record<string, Directive>
7677
inheritAttrs?: boolean
7778

79+
// Internal ------------------------------------------------------------------
80+
81+
// marker for AsyncComponentWrapper
82+
__asyncLoader?: () => Promise<Component>
83+
// cache for merged $options
84+
__merged?: ComponentOptions
85+
7886
// type-only differentiator to separate OptionWithoutProps from a constructor
7987
// type returned by defineComponent() or FunctionalComponent
8088
call?: never
81-
// marker for AsyncComponentWrapper
82-
__asyncLoader?: () => Promise<Component>
8389
// type-only differentiators for built-in Vnode types
8490
__isFragment?: never
8591
__isPortal?: never
@@ -161,7 +167,8 @@ export interface LegacyOptions<
161167
C extends ComputedOptions,
162168
M extends MethodOptions
163169
> {
164-
el?: any
170+
// allow any custom options
171+
[key: string]: any
165172

166173
// state
167174
// Limitation: we cannot expose RawBindings on the `this` context for data
@@ -501,3 +508,31 @@ function createWatcher(
501508
warn(`Invalid watch option: "${key}"`)
502509
}
503510
}
511+
512+
export function resolveMergedOptions(
513+
instance: ComponentInternalInstance
514+
): ComponentOptions {
515+
const raw = instance.type as ComponentOptions
516+
const { __merged, mixins, extends: extendsOptions } = raw
517+
if (__merged) return __merged
518+
const globalMixins = instance.appContext.mixins
519+
if (!globalMixins && !mixins && !extendsOptions) return raw
520+
const options = {}
521+
globalMixins && globalMixins.forEach(m => mergeOptions(options, m, instance))
522+
extendsOptions && mergeOptions(options, extendsOptions, instance)
523+
mixins && mixins.forEach(m => mergeOptions(options, m, instance))
524+
mergeOptions(options, raw, instance)
525+
return (raw.__merged = options)
526+
}
527+
528+
function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) {
529+
const strats = instance.appContext.config.optionMergeStrategies
530+
for (const key in from) {
531+
const strat = strats && strats[key]
532+
if (strat) {
533+
to[key] = strat(to[key], from[key], instance.proxy, key)
534+
} else if (!hasOwn(to, key)) {
535+
to[key] = from[key]
536+
}
537+
}
538+
}

packages/runtime-core/src/componentProxy.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
ExtractComputedReturns,
77
ComponentOptionsBase,
88
ComputedOptions,
9-
MethodOptions
9+
MethodOptions,
10+
resolveMergedOptions
1011
} from './apiOptions'
1112
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
1213
import { warn } from './warning'
@@ -61,7 +62,7 @@ const publicPropertiesMap: Record<
6162
$parent: i => i.parent,
6263
$root: i => i.root,
6364
$emit: i => i.emit,
64-
$options: i => i.type,
65+
$options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
6566
$forceUpdate: i => () => queueJob(i.update),
6667
$nextTick: () => nextTick,
6768
$watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP

packages/runtime-core/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ export {
163163
AppConfig,
164164
AppContext,
165165
Plugin,
166-
CreateAppFunction
166+
CreateAppFunction,
167+
OptionMergeFunction
167168
} from './apiCreateApp'
168169
export {
169170
VNode,

0 commit comments

Comments
 (0)