]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-core): support `config.optionMergeStrategies`
authorEvan You <yyx990803@gmail.com>
Tue, 24 Mar 2020 15:59:00 +0000 (11:59 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 24 Mar 2020 15:59:00 +0000 (11:59 -0400)
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.

packages/runtime-core/__tests__/apiOptions.spec.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/index.ts

index 836373c7053506bfdff7fc946bc92db798f20b4d..70952e17a0d2ef063ad21b774be65f5789a95cc3 100644 (file)
@@ -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(`<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', () => {
     mockWarn()
 
index 3c03250378f405d79acb5bc35c01e1e1f6412979..7c63f0b0deaf5a38c6fc759283757b50f52884a1 100644 (file)
@@ -36,11 +36,21 @@ export interface App<HostElement = any> {
   _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<string, OptionMergeFunction>
+  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
index aea558107de08aeee9d3872223b7fdc1c4e5eb20..a33e62fb22db0dd52840b94100e85b3efe5c5f98 100644 (file)
@@ -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<string, Directive>
   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 returned by defineComponent() or FunctionalComponent
   call?: never
-  // marker for AsyncComponentWrapper
-  __asyncLoader?: () => Promise<Component>
   // 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]
+    }
+  }
+}
index 78ba4339cd2410f891f9b1d8849698e8432df048..e40fb8019de3062e9088367ba4d0ae38768f6a8d 100644 (file)
@@ -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
index d6be3c56645581e0245288dc47641b79a40cdbde..f5fbb9af84e1673ae4e33098824996ed47ea2f21 100644 (file)
@@ -163,7 +163,8 @@ export {
   AppConfig,
   AppContext,
   Plugin,
-  CreateAppFunction
+  CreateAppFunction,
+  OptionMergeFunction
 } from './apiCreateApp'
 export {
   VNode,