From 528621ba41b1d7113940077574217d01d182b35f Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 24 Mar 2020 11:59:00 -0400 Subject: [PATCH] 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. --- .../runtime-core/__tests__/apiOptions.spec.ts | 25 ++++++++++- packages/runtime-core/src/apiCreateApp.ts | 17 ++++++-- packages/runtime-core/src/apiOptions.ts | 43 +++++++++++++++++-- packages/runtime-core/src/componentProxy.ts | 5 ++- packages/runtime-core/src/index.ts | 3 +- 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index 836373c7..70952e17 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -8,7 +8,8 @@ import { nextTick, renderToString, ref, - defineComponent + defineComponent, + createApp } from '@vue/runtime-test' import { mockWarn } from '@vue/shared' @@ -562,6 +563,28 @@ describe('api: options', () => { expect(serializeInner(root)).toBe(`
1,1,3
`) }) + 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', () => { mockWarn() diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 3c032503..7c63f0b0 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -36,11 +36,21 @@ export interface App { _context: AppContext } +export type OptionMergeFunction = ( + to: unknown, + from: unknown, + instance: any, + key: string +) => any + export interface AppConfig { + // @private + readonly isNativeTag?: (tag: string) => boolean + devtools: boolean performance: boolean - readonly isNativeTag?: (tag: string) => boolean - isCustomElement?: (tag: string) => boolean + optionMergeStrategies: Record + isCustomElement: (tag: string) => boolean errorHandler?: ( err: unknown, instance: ComponentPublicInstance | null, @@ -73,9 +83,10 @@ export type Plugin = export function createAppContext(): AppContext { return { config: { + isNativeTag: NO, devtools: true, performance: false, - isNativeTag: NO, + optionMergeStrategies: {}, isCustomElement: NO, errorHandler: undefined, warnHandler: undefined diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index aea55810..a33e62fb 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -14,7 +14,8 @@ import { isObject, isArray, EMPTY_OBJ, - NOOP + NOOP, + hasOwn } from '@vue/shared' import { computed } from './apiComputed' import { watch, WatchOptions, WatchCallback } from './apiWatch' @@ -75,11 +76,16 @@ export interface ComponentOptionsBase< directives?: Record inheritAttrs?: boolean + // Internal ------------------------------------------------------------------ + + // marker for AsyncComponentWrapper + __asyncLoader?: () => Promise + // cache for merged $options + __merged?: ComponentOptions + // type-only differentiator to separate OptionWithoutProps from a constructor // type returned by defineComponent() or FunctionalComponent call?: never - // marker for AsyncComponentWrapper - __asyncLoader?: () => Promise // type-only differentiators for built-in Vnode types __isFragment?: never __isPortal?: never @@ -161,7 +167,8 @@ export interface LegacyOptions< C extends ComputedOptions, M extends MethodOptions > { - el?: any + // allow any custom options + [key: string]: any // state // Limitation: we cannot expose RawBindings on the `this` context for data @@ -501,3 +508,31 @@ function createWatcher( 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] + } + } +} diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 78ba4339..e40fb801 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -6,7 +6,8 @@ import { ExtractComputedReturns, ComponentOptionsBase, ComputedOptions, - MethodOptions + MethodOptions, + resolveMergedOptions } from './apiOptions' import { ReactiveEffect, UnwrapRef } from '@vue/reactivity' import { warn } from './warning' @@ -61,7 +62,7 @@ const publicPropertiesMap: Record< $parent: i => i.parent, $root: i => i.root, $emit: i => i.emit, - $options: i => i.type, + $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type), $forceUpdate: i => () => queueJob(i.update), $nextTick: () => nextTick, $watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index d6be3c56..f5fbb9af 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -163,7 +163,8 @@ export { AppConfig, AppContext, Plugin, - CreateAppFunction + CreateAppFunction, + OptionMergeFunction } from './apiCreateApp' export { VNode,