From e52193d33659772f4704f55af96ba4cfd73c9185 Mon Sep 17 00:00:00 2001 From: Stas Lashmanov Date: Tue, 11 May 2021 23:53:10 +0300 Subject: [PATCH] fix: correctly merge lifecycle hooks when using Vue.extend --- .../runtime-core/src/compat/globalConfig.ts | 4 +-- packages/runtime-core/src/componentOptions.ts | 34 ++++++++++-------- packages/shared/src/index.ts | 4 +++ packages/vue-compat/__tests__/global.spec.ts | 19 +++++++--- packages/vue-compat/__tests__/options.spec.ts | 35 ++++++++++++++++++- 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/packages/runtime-core/src/compat/globalConfig.ts b/packages/runtime-core/src/compat/globalConfig.ts index bf7b9189e07..f4e831da357 100644 --- a/packages/runtime-core/src/compat/globalConfig.ts +++ b/packages/runtime-core/src/compat/globalConfig.ts @@ -1,4 +1,4 @@ -import { extend, isArray } from '@vue/shared' +import { extend, toArray } from '@vue/shared' import { AppConfig } from '../apiCreateApp' import { mergeDataOption } from './data' import { @@ -117,7 +117,7 @@ function mergeHook( to: Function[] | Function | undefined, from: Function | Function[] ) { - return Array.from(new Set([...(isArray(to) ? to : to ? [to] : []), from])) + return Array.from(new Set([...toArray(to), ...toArray(from)])) } function mergeObjectOptions(to: Object | undefined, from: Object | undefined) { diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 40f5669b508..8640ac750d0 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -768,40 +768,40 @@ export function applyOptions( ) } if (beforeMount) { - onBeforeMount(beforeMount.bind(publicThis)) + runLifecycleHook(onBeforeMount, beforeMount, publicThis) } if (mounted) { - onMounted(mounted.bind(publicThis)) + runLifecycleHook(onMounted, mounted, publicThis) } if (beforeUpdate) { - onBeforeUpdate(beforeUpdate.bind(publicThis)) + runLifecycleHook(onBeforeUpdate, beforeUpdate, publicThis) } if (updated) { - onUpdated(updated.bind(publicThis)) + runLifecycleHook(onUpdated, updated, publicThis) } if (activated) { - onActivated(activated.bind(publicThis)) + runLifecycleHook(onActivated, activated, publicThis) } if (deactivated) { - onDeactivated(deactivated.bind(publicThis)) + runLifecycleHook(onDeactivated, deactivated, publicThis) } if (errorCaptured) { - onErrorCaptured(errorCaptured.bind(publicThis)) + runLifecycleHook(onErrorCaptured, errorCaptured, publicThis) } if (renderTracked) { - onRenderTracked(renderTracked.bind(publicThis)) + runLifecycleHook(onRenderTracked, renderTracked, publicThis) } if (renderTriggered) { - onRenderTriggered(renderTriggered.bind(publicThis)) + runLifecycleHook(onRenderTriggered, renderTriggered, publicThis) } if (beforeUnmount) { - onBeforeUnmount(beforeUnmount.bind(publicThis)) + runLifecycleHook(onBeforeUnmount, beforeUnmount, publicThis) } if (unmounted) { - onUnmounted(unmounted.bind(publicThis)) + runLifecycleHook(onUnmounted, unmounted, publicThis) } if (serverPrefetch) { - onServerPrefetch(serverPrefetch.bind(publicThis)) + runLifecycleHook(onServerPrefetch, serverPrefetch, publicThis) } if (__COMPAT__) { @@ -809,13 +809,13 @@ export function applyOptions( beforeDestroy && softAssertCompatEnabled(DeprecationTypes.OPTIONS_BEFORE_DESTROY, instance) ) { - onBeforeUnmount(beforeDestroy.bind(publicThis)) + runLifecycleHook(onBeforeUnmount, beforeDestroy, publicThis) } if ( destroyed && softAssertCompatEnabled(DeprecationTypes.OPTIONS_DESTROYED, instance) ) { - onUnmounted(destroyed.bind(publicThis)) + runLifecycleHook(onUnmounted, destroyed, publicThis) } } @@ -835,6 +835,12 @@ export function applyOptions( } } +function runLifecycleHook(handler: Function, hook: Function | Function[], context: ComponentPublicInstance) { + // Array lifecycle hooks are only present in the compat build + if (__COMPAT__ && isArray(hook)) hook.forEach(_hook => handler(_hook.bind(context))) + else handler((hook as Function).bind(context)) +} + function resolveInstanceAssets( instance: ComponentInternalInstance, mixin: ComponentOptions, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 84b324beda3..252b8351393 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -180,3 +180,7 @@ export const getGlobalThis = (): any => { : {}) ) } + +export const toArray = (target: any) => { + return isArray(target) ? target : target ? [target] : [] +} diff --git a/packages/vue-compat/__tests__/global.spec.ts b/packages/vue-compat/__tests__/global.spec.ts index 02d578772b7..6ef28f1ddbf 100644 --- a/packages/vue-compat/__tests__/global.spec.ts +++ b/packages/vue-compat/__tests__/global.spec.ts @@ -145,22 +145,31 @@ describe('GLOBAL_EXTEND', () => { }) it('should not merge nested mixins created with Vue.extend', () => { + const a = jest.fn(); + const b = jest.fn(); + const c = jest.fn(); + const d = jest.fn(); const A = Vue.extend({ - created: () => {} + created: a }) const B = Vue.extend({ mixins: [A], - created: () => {} + created: b }) const C = Vue.extend({ extends: B, - created: () => {} + created: c }) const D = Vue.extend({ mixins: [C], - created: () => {} + created: d, + render() { return null }, }) - expect(D.options.created!.length).toBe(4) + new D().$mount() + expect(a.mock.calls.length).toStrictEqual(1) + expect(b.mock.calls.length).toStrictEqual(1) + expect(c.mock.calls.length).toStrictEqual(1) + expect(d.mock.calls.length).toStrictEqual(1) }) it('should merge methods', () => { diff --git a/packages/vue-compat/__tests__/options.spec.ts b/packages/vue-compat/__tests__/options.spec.ts index ca8ea807381..d0225dc964b 100644 --- a/packages/vue-compat/__tests__/options.spec.ts +++ b/packages/vue-compat/__tests__/options.spec.ts @@ -10,7 +10,8 @@ beforeEach(() => { toggleDeprecationWarning(true) Vue.configureCompat({ MODE: 2, - GLOBAL_MOUNT: 'suppress-warning' + GLOBAL_MOUNT: 'suppress-warning', + GLOBAL_EXTEND: 'suppress-warning' }) }) @@ -90,3 +91,35 @@ test('beforeDestroy/destroyed', async () => { deprecationData[DeprecationTypes.OPTIONS_DESTROYED].message ).toHaveBeenWarned() }) + +test('beforeDestroy/destroyed in Vue.extend components', async () => { + const beforeDestroy = jest.fn() + const destroyed = jest.fn() + + const child = Vue.extend({ + template: `foo`, + beforeDestroy, + destroyed + }) + + const vm = new Vue({ + template: ``, + data() { + return { ok: true } + }, + components: { child } + }).$mount() as any + + vm.ok = false + await nextTick() + expect(beforeDestroy).toHaveBeenCalled() + expect(destroyed).toHaveBeenCalled() + + expect( + deprecationData[DeprecationTypes.OPTIONS_BEFORE_DESTROY].message + ).toHaveBeenWarned() + + expect( + deprecationData[DeprecationTypes.OPTIONS_DESTROYED].message + ).toHaveBeenWarned() +})