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.
This commit is contained in:
Evan You 2020-03-24 11:59:00 -04:00
parent 123738727a
commit 528621ba41
5 changed files with 82 additions and 11 deletions

View File

@ -8,7 +8,8 @@ import {
nextTick, nextTick,
renderToString, renderToString,
ref, ref,
defineComponent defineComponent,
createApp
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared' import { mockWarn } from '@vue/shared'
@ -562,6 +563,28 @@ describe('api: options', () => {
expect(serializeInner(root)).toBe(`<div>1,1,3</div>`) expect(serializeInner(root)).toBe(`<div>1,1,3</div>`)
}) })
test('optionMergeStrategies', () => {
let merged: string
const App = defineComponent({
render() {},
mixins: [{ foo: 'mixin' }],
extends: { foo: 'extends' },
foo: 'local',
mounted() {
merged = this.$options.foo
}
})
const app = createApp(App)
app.mixin({
foo: 'global'
})
app.config.optionMergeStrategies.foo = (a, b) => (a ? `${a},` : ``) + b
app.mount(nodeOps.createElement('div'))
expect(merged!).toBe('global,extends,mixin,local')
})
describe('warnings', () => { describe('warnings', () => {
mockWarn() mockWarn()

View File

@ -36,11 +36,21 @@ export interface App<HostElement = any> {
_context: AppContext _context: AppContext
} }
export type OptionMergeFunction = (
to: unknown,
from: unknown,
instance: any,
key: string
) => any
export interface AppConfig { export interface AppConfig {
// @private
readonly isNativeTag?: (tag: string) => boolean
devtools: boolean devtools: boolean
performance: boolean performance: boolean
readonly isNativeTag?: (tag: string) => boolean optionMergeStrategies: Record<string, OptionMergeFunction>
isCustomElement?: (tag: string) => boolean isCustomElement: (tag: string) => boolean
errorHandler?: ( errorHandler?: (
err: unknown, err: unknown,
instance: ComponentPublicInstance | null, instance: ComponentPublicInstance | null,
@ -73,9 +83,10 @@ export type Plugin =
export function createAppContext(): AppContext { export function createAppContext(): AppContext {
return { return {
config: { config: {
isNativeTag: NO,
devtools: true, devtools: true,
performance: false, performance: false,
isNativeTag: NO, optionMergeStrategies: {},
isCustomElement: NO, isCustomElement: NO,
errorHandler: undefined, errorHandler: undefined,
warnHandler: undefined warnHandler: undefined

View File

@ -14,7 +14,8 @@ import {
isObject, isObject,
isArray, isArray,
EMPTY_OBJ, EMPTY_OBJ,
NOOP NOOP,
hasOwn
} from '@vue/shared' } from '@vue/shared'
import { computed } from './apiComputed' import { computed } from './apiComputed'
import { watch, WatchOptions, WatchCallback } from './apiWatch' import { watch, WatchOptions, WatchCallback } from './apiWatch'
@ -75,11 +76,16 @@ export interface ComponentOptionsBase<
directives?: Record<string, Directive> directives?: Record<string, Directive>
inheritAttrs?: boolean inheritAttrs?: boolean
// Internal ------------------------------------------------------------------
// marker for AsyncComponentWrapper
__asyncLoader?: () => Promise<Component>
// cache for merged $options
__merged?: ComponentOptions
// type-only differentiator to separate OptionWithoutProps from a constructor // type-only differentiator to separate OptionWithoutProps from a constructor
// type returned by defineComponent() or FunctionalComponent // type returned by defineComponent() or FunctionalComponent
call?: never call?: never
// marker for AsyncComponentWrapper
__asyncLoader?: () => Promise<Component>
// type-only differentiators for built-in Vnode types // type-only differentiators for built-in Vnode types
__isFragment?: never __isFragment?: never
__isPortal?: never __isPortal?: never
@ -161,7 +167,8 @@ export interface LegacyOptions<
C extends ComputedOptions, C extends ComputedOptions,
M extends MethodOptions M extends MethodOptions
> { > {
el?: any // allow any custom options
[key: string]: any
// state // state
// Limitation: we cannot expose RawBindings on the `this` context for data // Limitation: we cannot expose RawBindings on the `this` context for data
@ -501,3 +508,31 @@ function createWatcher(
warn(`Invalid watch option: "${key}"`) warn(`Invalid watch option: "${key}"`)
} }
} }
export function resolveMergedOptions(
instance: ComponentInternalInstance
): ComponentOptions {
const raw = instance.type as ComponentOptions
const { __merged, mixins, extends: extendsOptions } = raw
if (__merged) return __merged
const globalMixins = instance.appContext.mixins
if (!globalMixins && !mixins && !extendsOptions) return raw
const options = {}
globalMixins && globalMixins.forEach(m => mergeOptions(options, m, instance))
extendsOptions && mergeOptions(options, extendsOptions, instance)
mixins && mixins.forEach(m => mergeOptions(options, m, instance))
mergeOptions(options, raw, instance)
return (raw.__merged = options)
}
function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) {
const strats = instance.appContext.config.optionMergeStrategies
for (const key in from) {
const strat = strats && strats[key]
if (strat) {
to[key] = strat(to[key], from[key], instance.proxy, key)
} else if (!hasOwn(to, key)) {
to[key] = from[key]
}
}
}

View File

@ -6,7 +6,8 @@ import {
ExtractComputedReturns, ExtractComputedReturns,
ComponentOptionsBase, ComponentOptionsBase,
ComputedOptions, ComputedOptions,
MethodOptions MethodOptions,
resolveMergedOptions
} from './apiOptions' } from './apiOptions'
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity' import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
import { warn } from './warning' import { warn } from './warning'
@ -61,7 +62,7 @@ const publicPropertiesMap: Record<
$parent: i => i.parent, $parent: i => i.parent,
$root: i => i.root, $root: i => i.root,
$emit: i => i.emit, $emit: i => i.emit,
$options: i => i.type, $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => () => queueJob(i.update), $forceUpdate: i => () => queueJob(i.update),
$nextTick: () => nextTick, $nextTick: () => nextTick,
$watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP $watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP

View File

@ -163,7 +163,8 @@ export {
AppConfig, AppConfig,
AppContext, AppContext,
Plugin, Plugin,
CreateAppFunction CreateAppFunction,
OptionMergeFunction
} from './apiCreateApp' } from './apiCreateApp'
export { export {
VNode, VNode,