]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: tests for global api compat
authorEvan You <yyx990803@gmail.com>
Tue, 27 Apr 2021 21:34:19 +0000 (17:34 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 27 Apr 2021 21:34:19 +0000 (17:34 -0400)
packages/runtime-core/src/compat/__tests__/global.spec.ts
packages/runtime-core/src/compat/compatConfig.ts
packages/runtime-core/src/compat/data.ts
packages/runtime-core/src/compat/global.ts
packages/runtime-core/src/compat/globalConfig.ts
packages/runtime-core/src/compat/instance.ts
packages/runtime-core/src/componentOptions.ts

index 9bd042577f6d507099d15efa8b5c930b7ffa17d9..1ce38e4166d3a0fbb3970f4d68a197d3112b431a 100644 (file)
 import Vue from '@vue/compat'
+import { effect, isReactive } from '@vue/reactivity'
+import {
+  DeprecationTypes,
+  deprecationData,
+  toggleDeprecationWarning
+} from '../compatConfig'
 
-describe('compat: global API', () => {
-  beforeEach(() => Vue.configureCompat({ MODE: 2 }))
-  afterEach(() => Vue.configureCompat({ MODE: 3 }))
+beforeEach(() => {
+  Vue.configureCompat({ MODE: 2 })
+})
+
+afterEach(() => {
+  Vue.configureCompat({ MODE: 3 })
+  toggleDeprecationWarning(false)
+})
+
+describe('GLOBAL_MOUNT', () => {
+  test('new Vue() with el', () => {
+    toggleDeprecationWarning(true)
 
-  test('should work', () => {
     const el = document.createElement('div')
     el.innerHTML = `{{ msg }}`
     new Vue({
       el,
+      compatConfig: { GLOBAL_MOUNT: true },
       data() {
         return {
           msg: 'hello'
         }
       }
     })
-    expect('global app bootstrapping API has changed').toHaveBeenWarned()
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
+    ).toHaveBeenWarned()
     expect(el.innerHTML).toBe('hello')
   })
+
+  test('new Vue() + $mount', () => {
+    const el = document.createElement('div')
+    el.innerHTML = `{{ msg }}`
+    new Vue({
+      data() {
+        return {
+          msg: 'hello'
+        }
+      }
+    }).$mount(el)
+    expect(el.innerHTML).toBe('hello')
+  })
+})
+
+describe('GLOBAL_MOUNT_CONTAINER', () => {
+  test('should warn', () => {
+    toggleDeprecationWarning(true)
+
+    const el = document.createElement('div')
+    el.innerHTML = `test`
+    el.setAttribute('v-bind:id', 'foo')
+    new Vue().$mount(el)
+    // warning only
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
+    ).toHaveBeenWarned()
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_MOUNT_CONTAINER].message
+    ).toHaveBeenWarned()
+  })
+})
+
+describe('GLOBAL_EXTEND', () => {
+  // https://github.com/vuejs/vue/blob/dev/test/unit/features/global-api/extend.spec.js
+  it('should correctly merge options', () => {
+    toggleDeprecationWarning(true)
+
+    const Test = Vue.extend({
+      name: 'test',
+      a: 1,
+      b: 2
+    })
+    expect(Test.options.a).toBe(1)
+    expect(Test.options.b).toBe(2)
+    expect(Test.super).toBe(Vue)
+    const t = new Test({
+      a: 2
+    })
+    expect(t.$options.a).toBe(2)
+    expect(t.$options.b).toBe(2)
+    // inheritance
+    const Test2 = Test.extend({
+      a: 2
+    })
+    expect(Test2.options.a).toBe(2)
+    expect(Test2.options.b).toBe(2)
+    const t2 = new Test2({
+      a: 3
+    })
+    expect(t2.$options.a).toBe(3)
+    expect(t2.$options.b).toBe(2)
+
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
+    ).toHaveBeenWarned()
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_EXTEND].message
+    ).toHaveBeenWarned()
+  })
+
+  it('should work when used as components', () => {
+    const foo = Vue.extend({
+      template: '<span>foo</span>'
+    })
+    const bar = Vue.extend({
+      template: '<span>bar</span>'
+    })
+    const vm = new Vue({
+      template: '<div><foo></foo><bar></bar></div>',
+      components: { foo, bar }
+    }).$mount()
+    expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
+  })
+
+  it('should merge lifecycle hooks', () => {
+    const calls: number[] = []
+    const A = Vue.extend({
+      created() {
+        calls.push(1)
+      }
+    })
+    const B = A.extend({
+      created() {
+        calls.push(2)
+      }
+    })
+    new B({
+      created() {
+        calls.push(3)
+      }
+    })
+    expect(calls).toEqual([1, 2, 3])
+  })
+
+  it('should not merge nested mixins created with Vue.extend', () => {
+    const A = Vue.extend({
+      created: () => {}
+    })
+    const B = Vue.extend({
+      mixins: [A],
+      created: () => {}
+    })
+    const C = Vue.extend({
+      extends: B,
+      created: () => {}
+    })
+    const D = Vue.extend({
+      mixins: [C],
+      created: () => {}
+    })
+    expect(D.options.created!.length).toBe(4)
+  })
+
+  it('should merge methods', () => {
+    const A = Vue.extend({
+      methods: {
+        a() {
+          return this.n
+        }
+      }
+    })
+    const B = A.extend({
+      methods: {
+        b() {
+          return this.n + 1
+        }
+      }
+    })
+    const b = new B({
+      data: () => ({ n: 0 }),
+      methods: {
+        c() {
+          return this.n + 2
+        }
+      }
+    }) as any
+    expect(b.a()).toBe(0)
+    expect(b.b()).toBe(1)
+    expect(b.c()).toBe(2)
+  })
+
+  it('should merge assets', () => {
+    const A = Vue.extend({
+      components: {
+        aa: {
+          template: '<div>A</div>'
+        }
+      }
+    })
+    const B = A.extend({
+      components: {
+        bb: {
+          template: '<div>B</div>'
+        }
+      }
+    })
+    const b = new B({
+      template: '<div><aa></aa><bb></bb></div>'
+    }).$mount()
+    expect(b.$el.innerHTML).toBe('<div>A</div><div>B</div>')
+  })
+
+  it('caching', () => {
+    const options = {
+      template: '<div></div>'
+    }
+    const A = Vue.extend(options)
+    const B = Vue.extend(options)
+    expect(A).toBe(B)
+  })
+
+  it('extended options should use different identify from parent', () => {
+    const A = Vue.extend({ computed: {} })
+    const B = A.extend()
+    B.options.computed.b = () => 'foo'
+    expect(B.options.computed).not.toBe(A.options.computed)
+    expect(A.options.computed.b).toBeUndefined()
+  })
+})
+
+describe('GLOBAL_PROTOTYPE', () => {
+  test('plain properties', () => {
+    toggleDeprecationWarning(true)
+    Vue.prototype.$test = 1
+    const vm = new Vue() as any
+    expect(vm.$test).toBe(1)
+    delete Vue.prototype.$test
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
+    ).toHaveBeenWarned()
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_PROTOTYPE].message
+    ).toHaveBeenWarned()
+  })
+
+  test('method this context', () => {
+    Vue.prototype.$test = function() {
+      return this.msg
+    }
+    const vm = new Vue({
+      data() {
+        return { msg: 'method' }
+      }
+    }) as any
+    expect(vm.$test()).toBe('method')
+    delete Vue.prototype.$test
+  })
+
+  test('defined properties', () => {
+    Object.defineProperty(Vue.prototype, '$test', {
+      configurable: true,
+      get() {
+        return this.msg
+      }
+    })
+    const vm = new Vue({
+      data() {
+        return { msg: 'getter' }
+      }
+    }) as any
+    expect(vm.$test).toBe('getter')
+    delete Vue.prototype.$test
+  })
+
+  test('extended prototype', async () => {
+    const Foo = Vue.extend()
+    Foo.prototype.$test = 1
+    const vm = new Foo() as any
+    expect(vm.$test).toBe(1)
+    const plain = new Vue() as any
+    expect(plain.$test).toBeUndefined()
+  })
+})
+
+describe('GLOBAL_SET/DELETE', () => {
+  test('set', () => {
+    toggleDeprecationWarning(true)
+    const obj: any = {}
+    Vue.set(obj, 'foo', 1)
+    expect(obj.foo).toBe(1)
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_SET].message
+    ).toHaveBeenWarned()
+  })
+
+  test('delete', () => {
+    toggleDeprecationWarning(true)
+    const obj: any = { foo: 1 }
+    Vue.delete(obj, 'foo')
+    expect('foo' in obj).toBe(false)
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_DELETE].message
+    ).toHaveBeenWarned()
+  })
+})
+
+describe('GLOBAL_OBSERVABLE', () => {
+  test('should work', () => {
+    toggleDeprecationWarning(true)
+    const obj = Vue.observable({})
+    expect(isReactive(obj)).toBe(true)
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_OBSERVABLE].message
+    ).toHaveBeenWarned()
+  })
+})
+
+describe('GLOBAL_PRIVATE_UTIL', () => {
+  test('defineReactive', () => {
+    toggleDeprecationWarning(true)
+    const obj: any = {}
+    // @ts-ignore
+    Vue.util.defineReactive(obj, 'test', 1)
+
+    let n
+    effect(() => {
+      n = obj.test
+    })
+    expect(n).toBe(1)
+    obj.test++
+    expect(n).toBe(2)
+
+    expect(
+      deprecationData[DeprecationTypes.GLOBAL_PRIVATE_UTIL].message
+    ).toHaveBeenWarned()
+  })
 })
index a163bf29420f9083a3134a5b3088fc4eeba35dc8..e59bdb0a2330d3d9a093211e392b61223cac4bba 100644 (file)
@@ -17,7 +17,7 @@ export const enum DeprecationTypes {
   GLOBAL_SET = 'GLOBAL_SET',
   GLOBAL_DELETE = 'GLOBAL_DELETE',
   GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE',
-  GLOBAL_UTIL = 'GLOBAL_UTIL',
+  GLOBAL_PRIVATE_UTIL = 'GLOBAL_PRIVATE_UTIL',
 
   CONFIG_SILENT = 'CONFIG_SILENT',
   CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS',
@@ -70,7 +70,7 @@ type DeprecationData = {
   link?: string
 }
 
-const deprecationData: Record<DeprecationTypes, DeprecationData> = {
+export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
   [DeprecationTypes.GLOBAL_MOUNT]: {
     message:
       `The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
@@ -119,7 +119,7 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
     link: `https://v3.vuejs.org/api/basic-reactivity.html`
   },
 
-  [DeprecationTypes.GLOBAL_UTIL]: {
+  [DeprecationTypes.GLOBAL_PRIVATE_UTIL]: {
     message:
       `Vue.util has been removed. Please refactor to avoid its usage ` +
       `since it was an internal API even in Vue 2.`
@@ -437,6 +437,13 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
 const instanceWarned: Record<string, true> = Object.create(null)
 const warnCount: Record<string, number> = Object.create(null)
 
+// test only
+let warningEnabled = true
+
+export function toggleDeprecationWarning(flag: boolean) {
+  warningEnabled = flag
+}
+
 export function warnDeprecation(
   key: DeprecationTypes,
   instance: ComponentInternalInstance | null,
@@ -445,6 +452,9 @@ export function warnDeprecation(
   if (!__DEV__) {
     return
   }
+  if (__TEST__ && !warningEnabled) {
+    return
+  }
 
   instance = instance || getCurrentInstance()
 
@@ -463,14 +473,14 @@ export function warnDeprecation(
 
   // skip if the same warning is emitted for the same component type
   const componentDupKey = dupKey + compId
-  if (componentDupKey in instanceWarned) {
+  if (!__TEST__ && componentDupKey in instanceWarned) {
     return
   }
   instanceWarned[componentDupKey] = true
 
   // same warning, but different component. skip the long message and just
   // log the key and count.
-  if (dupKey in warnCount) {
+  if (!__TEST__ && dupKey in warnCount) {
     warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`)
     return
   }
index 87663dbd03665d1ae906f8460162351a9537bacc..121c0f9a688a4eca0f54852ae0e6cf3743a5d3e8 100644 (file)
@@ -1,5 +1,6 @@
-import { isPlainObject } from '@vue/shared'
+import { isFunction, isPlainObject } from '@vue/shared'
 import { ComponentInternalInstance } from '../component'
+import { ComponentPublicInstance } from '../componentPublicInstance'
 import { DeprecationTypes, warnDeprecation } from './compatConfig'
 
 export function deepMergeData(
@@ -19,3 +20,19 @@ export function deepMergeData(
     }
   }
 }
+
+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.$
+    )
+  }
+}
index 16f3a9a597f96df3e50ce5a737570f3534e0001d..b8db418655532961ed935a645973a5993e8b9f24 100644 (file)
@@ -25,7 +25,6 @@ import {
   CreateAppFunction,
   Plugin
 } from '../apiCreateApp'
-import { defineComponent } from '../apiDefineComponent'
 import {
   Component,
   ComponentOptions,
@@ -40,7 +39,7 @@ import { devtoolsInitApp } from '../devtools'
 import { Directive } from '../directives'
 import { nextTick } from '../scheduler'
 import { version } from '..'
-import { LegacyConfig } from './globalConfig'
+import { LegacyConfig, legacyOptionMergeStrats } from './globalConfig'
 import { LegacyDirective } from './customDirective'
 import {
   warnDeprecation,
@@ -50,6 +49,7 @@ import {
   isCompatEnabled,
   softAssertCompatEnabled
 } from './compatConfig'
+import { LegacyPublicInstance } from './instance'
 
 /**
  * @deprecated the default `Vue` export has been removed in Vue 3. The type for
@@ -57,15 +57,17 @@ import {
  * named imports instead - e.g. `import { createApp } from 'vue'`.
  */
 export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
+  configureCompat: typeof configureCompat
+
   // no inference here since these types are not meant for actual use - they
   // are merely here to provide type checks for internal implementation and
   // information for migration.
-  new (options?: ComponentOptions): ComponentPublicInstance
+  new (options?: ComponentOptions): LegacyPublicInstance
 
   version: string
   config: AppConfig & LegacyConfig
 
-  extend: typeof defineComponent
+  extend: (options?: ComponentOptions) => CompatVue
   nextTick: typeof nextTick
 
   use(plugin: Plugin, ...options: any[]): CompatVue
@@ -102,8 +104,10 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
    * @internal
    */
   options: ComponentOptions
-
-  configureCompat: typeof configureCompat
+  /**
+   * @internal
+   */
+  super: CompatVue
 }
 
 export let isCopyingConfig = false
@@ -184,32 +188,55 @@ export function createCompatVue(
   let cid = 1
   Vue.cid = cid
 
+  const extendCache = new WeakMap()
+
   function extendCtor(this: any, extendOptions: ComponentOptions = {}) {
     assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null)
     if (isFunction(extendOptions)) {
       extendOptions = extendOptions.options
     }
 
+    if (extendCache.has(extendOptions)) {
+      return extendCache.get(extendOptions)
+    }
+
     const Super = this
     function SubVue(inlineOptions?: ComponentOptions) {
       if (!inlineOptions) {
-        return createCompatApp(extendOptions, SubVue)
+        return createCompatApp(SubVue.options, SubVue)
       } else {
         return createCompatApp(
-          {
-            el: inlineOptions.el,
-            extends: extendOptions,
-            mixins: [inlineOptions]
-          },
+          mergeOptions(
+            extend({}, SubVue.options),
+            inlineOptions,
+            null,
+            legacyOptionMergeStrats as any
+          ),
           SubVue
         )
       }
     }
+    SubVue.super = Super
     SubVue.prototype = Object.create(Vue.prototype)
     SubVue.prototype.constructor = SubVue
+
+    // clone non-primitive base option values for edge case of mutating
+    // extended options
+    const mergeBase: any = {}
+    for (const key in Super.options) {
+      const superValue = Super.options[key]
+      mergeBase[key] = isArray(superValue)
+        ? superValue.slice()
+        : isObject(superValue)
+          ? extend(Object.create(null), superValue)
+          : superValue
+    }
+
     SubVue.options = mergeOptions(
-      extend({}, Super.options) as ComponentOptions,
-      extendOptions
+      mergeBase,
+      extendOptions,
+      null,
+      legacyOptionMergeStrats as any
     )
 
     SubVue.options._base = SubVue
@@ -217,6 +244,8 @@ export function createCompatVue(
     SubVue.mixin = Super.mixin
     SubVue.use = Super.use
     SubVue.cid = ++cid
+
+    extendCache.set(extendOptions, SubVue)
     return SubVue
   }
 
@@ -279,12 +308,17 @@ export function createCompatVue(
     warn: __DEV__ ? warn : NOOP,
     extend,
     mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
-      mergeOptions(parent, child, vm && vm.$),
+      mergeOptions(
+        parent,
+        child,
+        vm && vm.$,
+        vm ? undefined : (legacyOptionMergeStrats as any)
+      ),
     defineReactive
   }
   Object.defineProperty(Vue, 'util', {
     get() {
-      assertCompatEnabled(DeprecationTypes.GLOBAL_UTIL, null)
+      assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
       return util
     }
   })
@@ -332,7 +366,7 @@ export function installCompatMount(
     // Note: the following assumes DOM environment since the compat build
     // only targets web. It essentially includes logic for app.mount from
     // both runtime-core AND runtime-dom.
-    instance.ctx._compat_mount = (selectorOrEl: string | Element) => {
+    instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
       if (isMounted) {
         __DEV__ && warn(`Root instance is already mounted.`)
         return
@@ -351,14 +385,8 @@ export function installCompatMount(
         }
         container = result
       } else {
-        if (!selectorOrEl) {
-          __DEV__ &&
-            warn(
-              `Failed to mount root instance: invalid mount target ${selectorOrEl}.`
-            )
-          return
-        }
-        container = selectorOrEl
+        // eslint-disable-next-line
+        container = selectorOrEl || document.createElement('div')
       }
 
       const isSVG = container instanceof SVGElement
index 168ba87cc3e4da4644c96578811fa04e29b07815..ae6664f23a6cb415a8c91748b462450c76201e5e 100644 (file)
@@ -1,7 +1,7 @@
 import { extend, isArray, isString } from '@vue/shared'
 import { AppConfig } from '../apiCreateApp'
 import { isRuntimeOnly } from '../component'
-import { deepMergeData } from './data'
+import { mergeDataOption } from './data'
 import {
   DeprecationTypes,
   warnDeprecation,
@@ -80,34 +80,36 @@ export function installLegacyConfigProperties(config: AppConfig) {
   // Internal merge strats which are no longer needed in v3, but we need to
   // expose them because some v2 plugins will reuse these internal strats to
   // merge their custom options.
-  const strats = config.optionMergeStrategies as any
-  strats.data = deepMergeData
-  // lifecycle hooks
-  strats.beforeCreate = mergeHook
-  strats.created = mergeHook
-  strats.beforeMount = mergeHook
-  strats.mounted = mergeHook
-  strats.beforeUpdate = mergeHook
-  strats.updated = mergeHook
-  strats.beforeDestroy = mergeHook
-  strats.destroyed = mergeHook
-  strats.activated = mergeHook
-  strats.deactivated = mergeHook
-  strats.errorCaptured = mergeHook
-  strats.serverPrefetch = mergeHook
+  extend(config.optionMergeStrategies, legacyOptionMergeStrats)
+}
+
+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
-  strats.components = mergeObjectOptions
-  strats.directives = mergeObjectOptions
-  strats.filters = mergeObjectOptions
+  components: mergeObjectOptions,
+  directives: mergeObjectOptions,
+  filters: mergeObjectOptions,
   // objects
-  strats.props = mergeObjectOptions
-  strats.methods = mergeObjectOptions
-  strats.inject = mergeObjectOptions
-  strats.computed = mergeObjectOptions
+  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.
-  strats.watch = mergeObjectOptions
+  watch: mergeObjectOptions
 }
 
 function mergeHook(
index 208825cca74494e4a99db121bf662a0a9345cf36..10c59cebb68a3f52ea46ff12399715311e038032 100644 (file)
@@ -6,7 +6,10 @@ import {
   toDisplayString,
   toNumber
 } from '@vue/shared'
-import { PublicPropertiesMap } from '../componentPublicInstance'
+import {
+  ComponentPublicInstance,
+  PublicPropertiesMap
+} from '../componentPublicInstance'
 import { getCompatChildren } from './instanceChildren'
 import {
   DeprecationTypes,
@@ -33,6 +36,23 @@ import {
 } from './renderHelpers'
 import { resolveFilter } from '../helpers/resolveAssets'
 import { resolveMergedOptions } from '../componentOptions'
+import { Slots } from '../componentSlots'
+
+export type LegacyPublicInstance = ComponentPublicInstance &
+  LegacyPublicProperties
+
+export interface LegacyPublicProperties {
+  $set(target: object, key: string, value: any): void
+  $delete(target: object, key: string): void
+  $mount(el?: string | Element): this
+  $destroy(): void
+  $scopedSlots: Slots
+  $on(event: string | string[], fn: Function): this
+  $once(event: string, fn: Function): this
+  $off(event?: string, fn?: Function): this
+  $children: LegacyPublicProperties[]
+  $listeners: Record<string, Function | Function[]>
+}
 
 export function installCompatInstanceProperties(map: PublicPropertiesMap) {
   const set = (target: any, key: any, val: any) => {
index a9961d753a4a30cb22bd0d79916d6941eadd25e6..4222d063e1482329de7e8d4f42d2e214643cdfec 100644 (file)
@@ -893,7 +893,13 @@ function callHookWithMixinAndExtends(
     }
   }
   if (selfHook) {
-    callWithAsyncErrorHandling(selfHook.bind(instance.proxy!), instance, type)
+    callWithAsyncErrorHandling(
+      __COMPAT__ && isArray(selfHook)
+        ? selfHook.map(h => h.bind(instance.proxy!))
+        : selfHook.bind(instance.proxy!),
+      instance,
+      type
+    )
   }
 }
 
@@ -1007,21 +1013,23 @@ export function resolveMergedOptions(
 export function mergeOptions(
   to: any,
   from: any,
-  instance?: ComponentInternalInstance
+  instance?: ComponentInternalInstance | null,
+  strats = instance && instance.appContext.config.optionMergeStrategies
 ) {
   if (__COMPAT__ && isFunction(from)) {
     from = from.options
   }
 
-  const strats = instance && instance.appContext.config.optionMergeStrategies
   const { mixins, extends: extendsOptions } = from
 
-  extendsOptions && mergeOptions(to, extendsOptions, instance)
+  extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
   mixins &&
-    mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance))
+    mixins.forEach((m: ComponentOptionsMixin) =>
+      mergeOptions(to, m, instance, strats)
+    )
 
   for (const key in from) {
-    if (strats && hasOwn(to, key) && hasOwn(strats, key)) {
+    if (strats && hasOwn(strats, key)) {
       to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
     } else {
       to[key] = from[key]