From 55c3c8d7a6ed166b48320ba0493c08c5e72929f7 Mon Sep 17 00:00:00 2001 From: Thorsten Luenborg Date: Fri, 17 Sep 2021 10:37:36 +0200 Subject: [PATCH 1/6] feat(runtime-core): Plugins can return cleanup function which is ran at unmount --- .../__tests__/apiCreateApp.spec.ts | 31 +++++++++++++++++++ packages/runtime-core/src/apiCreateApp.ts | 14 +++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index e5e9a5906b1..989000f5ce6 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -313,6 +313,37 @@ describe('api: createApp', () => { ).toHaveBeenWarnedTimes(1) }) + test('use: call cleanup plugin on unmount', () => { + const cleanup = jest.fn().mockName('plugin cleanup') + const PluginA: Plugin = app => { + app.provide('foo', 1) + return cleanup + } + const PluginB: Plugin = { + install: (app, arg1, arg2) => { + app.provide('bar', arg1 + arg2) + return cleanup + } + } + + // should ignore non-function return values + const PluginC: Plugin = app => ({}) + + const app = createApp({ + render: () => `Test` + }) + app.use(PluginA) + app.use(PluginB) + app.use(PluginC) + + const root = nodeOps.createElement('div') + app.mount(root) + + app.unmount() + + expect(cleanup).toHaveBeenCalledTimes(2) + }) + test('config.errorHandler', () => { const error = new Error() const count = ref(0) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 3e438107da0..67ceaa6e39f 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -185,6 +185,7 @@ export function createAppAPI( const context = createAppContext() const installedPlugins = new Set() + const pluginCleanupFns: Array<() => any> = [] let isMounted = false @@ -211,20 +212,28 @@ export function createAppAPI( }, use(plugin: Plugin, ...options: any[]) { + let cleanupFn: (() => any) | undefined = undefined if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) - plugin.install(app, ...options) + cleanupFn = plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) - plugin(app, ...options) + cleanupFn = plugin(app, ...options) } else if (__DEV__) { warn( `A plugin must either be a function or an object with an "install" ` + `function.` ) } + if ( + cleanupFn && + typeof cleanupFn === 'function' && + cleanupFn.length === 0 + ) { + pluginCleanupFns.push(cleanupFn) + } return app }, @@ -323,6 +332,7 @@ export function createAppAPI( unmount() { if (isMounted) { render(null, app._container) + pluginCleanupFns.map(fn => fn()) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { app._instance = null devtoolsUnmountApp(app) From 016202b9d862763180d39db6f06c391b6cdbbda8 Mon Sep 17 00:00:00 2001 From: Thorsten Luenborg Date: Wed, 22 Sep 2021 20:23:14 +0200 Subject: [PATCH 2/6] refactor: use new app.onUnmount) hook instead of return value, which was too brittle --- .../__tests__/apiCreateApp.spec.ts | 15 +++++------ packages/runtime-core/src/apiCreateApp.ts | 27 +++++++++++-------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index 989000f5ce6..59960f67439 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -313,35 +313,34 @@ describe('api: createApp', () => { ).toHaveBeenWarnedTimes(1) }) - test('use: call cleanup plugin on unmount', () => { + test.only('onUnmount', () => { const cleanup = jest.fn().mockName('plugin cleanup') const PluginA: Plugin = app => { app.provide('foo', 1) - return cleanup + app.onUnmount(cleanup) } const PluginB: Plugin = { install: (app, arg1, arg2) => { app.provide('bar', arg1 + arg2) - return cleanup + app.onUnmount(cleanup) } } - // should ignore non-function return values - const PluginC: Plugin = app => ({}) - const app = createApp({ render: () => `Test` }) app.use(PluginA) app.use(PluginB) - app.use(PluginC) const root = nodeOps.createElement('div') app.mount(root) + //also can be added after mount + app.onUnmount(cleanup) + app.unmount() - expect(cleanup).toHaveBeenCalledTimes(2) + expect(cleanup).toHaveBeenCalledTimes(3) }) test('config.errorHandler', () => { diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 67ceaa6e39f..cbda776aab5 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -39,6 +39,7 @@ export interface App { isSVG?: boolean ): ComponentPublicInstance unmount(): void + onUnmount(cb: () => void): void provide(key: InjectionKey | string, value: T): this // internal, but we need to expose these for the server-renderer and devtools @@ -212,28 +213,20 @@ export function createAppAPI( }, use(plugin: Plugin, ...options: any[]) { - let cleanupFn: (() => any) | undefined = undefined if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) - cleanupFn = plugin.install(app, ...options) + plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) - cleanupFn = plugin(app, ...options) + plugin(app, ...options) } else if (__DEV__) { warn( `A plugin must either be a function or an object with an "install" ` + `function.` ) } - if ( - cleanupFn && - typeof cleanupFn === 'function' && - cleanupFn.length === 0 - ) { - pluginCleanupFns.push(cleanupFn) - } return app }, @@ -329,10 +322,22 @@ export function createAppAPI( } }, + onUnmount(cleanupFn: () => void) { + if (typeof cleanupFn === 'function') pluginCleanupFns.push(cleanupFn) + else if (__DEV__) { + warn( + `Expected function as first argument to app.onUnmount(), but got ${typeof cleanupFn}` + ) + } + }, unmount() { if (isMounted) { render(null, app._container) - pluginCleanupFns.map(fn => fn()) + pluginCleanupFns.map(fn => { + try { + fn() + } catch {} + }) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { app._instance = null devtoolsUnmountApp(app) From 130461248b79aea7e82c2079213685abf22554a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Mon, 27 Sep 2021 17:20:19 +0200 Subject: [PATCH 3/6] Update packages/runtime-core/__tests__/apiCreateApp.spec.ts Co-authored-by: Evan You --- packages/runtime-core/__tests__/apiCreateApp.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index 59960f67439..973f4f7057d 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -313,7 +313,7 @@ describe('api: createApp', () => { ).toHaveBeenWarnedTimes(1) }) - test.only('onUnmount', () => { + test('onUnmount', () => { const cleanup = jest.fn().mockName('plugin cleanup') const PluginA: Plugin = app => { app.provide('foo', 1) From 082c840a3474d3fd64a3a752faaa3e4d216ce2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Mon, 27 Sep 2021 17:20:26 +0200 Subject: [PATCH 4/6] Update packages/runtime-core/src/apiCreateApp.ts Co-authored-by: Evan You --- packages/runtime-core/src/apiCreateApp.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index cbda776aab5..1ca4c5f31e0 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -330,6 +330,7 @@ export function createAppAPI( ) } }, + unmount() { if (isMounted) { render(null, app._container) From dac0f5a2aeacc1a1ffeeeb51f11640bee6f94e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Fri, 20 Oct 2023 17:31:27 +0200 Subject: [PATCH 5/6] Update apiCreateApp.spec.ts replace jest with vi --- packages/runtime-core/__tests__/apiCreateApp.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiCreateApp.spec.ts b/packages/runtime-core/__tests__/apiCreateApp.spec.ts index 89935f9207b..555412c1d4d 100644 --- a/packages/runtime-core/__tests__/apiCreateApp.spec.ts +++ b/packages/runtime-core/__tests__/apiCreateApp.spec.ts @@ -335,7 +335,7 @@ describe('api: createApp', () => { }) test('onUnmount', () => { - const cleanup = jest.fn().mockName('plugin cleanup') + const cleanup = vi.fn().mockName('plugin cleanup') const PluginA: Plugin = app => { app.provide('foo', 1) app.onUnmount(cleanup) From c71d408c22d98d00b806380e017a8fab8fa6598d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 29 Apr 2024 18:37:39 +0800 Subject: [PATCH 6/6] refactor: handle error in unmount hooks --- packages/runtime-core/src/apiCreateApp.ts | 40 +++++++--------------- packages/runtime-core/src/errorHandling.ts | 6 ++-- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 95fd7f27b4f..ea77e94e271 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -25,8 +25,9 @@ import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { isFunction, NO, isObject, extend } from '@vue/shared' import { version } from '.' import { installAppCompatProperties } from './compat/global' -import { NormalizedPropsOptions } from './componentProps' -import { ObjectEmitsOptions } from './componentEmits' +import type { NormalizedPropsOptions } from './componentProps' +import type { ObjectEmitsOptions } from './componentEmits' +import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling' export interface App { version: string @@ -212,24 +213,8 @@ export function createAppAPI( } const context = createAppContext() - const pluginCleanupFns: Array<() => any> = [] - - // TODO remove in 3.4 - if (__DEV__) { - Object.defineProperty(context.config, 'unwrapInjectedRef', { - get() { - return true - }, - set() { - warn( - `app.config.unwrapInjectedRef has been deprecated. ` + - `3.3 now always unwraps injected refs in Options API.` - ) - } - }) - } - const installedPlugins = new WeakSet() + const pluginCleanupFns: Array<() => any> = [] let isMounted = false @@ -371,22 +356,23 @@ export function createAppAPI( }, onUnmount(cleanupFn: () => void) { - if (typeof cleanupFn === 'function') pluginCleanupFns.push(cleanupFn) - else if (__DEV__) { + if (__DEV__ && typeof cleanupFn !== 'function') { warn( - `Expected function as first argument to app.onUnmount(), but got ${typeof cleanupFn}` + `Expected function as first argument to app.onUnmount(), ` + + `but got ${typeof cleanupFn}` ) } + pluginCleanupFns.push(cleanupFn) }, unmount() { if (isMounted) { + callWithAsyncErrorHandling( + pluginCleanupFns, + app._instance, + ErrorCodes.APP_UNMOUNT_CLEANUP + ) render(null, app._container) - pluginCleanupFns.map(fn => { - try { - fn() - } catch {} - }) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { app._instance = null devtoolsUnmountApp(app) diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index afbd226c4c6..4731626b038 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -21,7 +21,8 @@ export const enum ErrorCodes { APP_WARN_HANDLER, FUNCTION_REF, ASYNC_COMPONENT_LOADER, - SCHEDULER + SCHEDULER, + APP_UNMOUNT_CLEANUP } export const ErrorTypeStrings: Record = { @@ -55,7 +56,8 @@ export const ErrorTypeStrings: Record = { [ErrorCodes.ASYNC_COMPONENT_LOADER]: 'async component loader', [ErrorCodes.SCHEDULER]: 'scheduler flush. This is likely a Vue internals bug. ' + - 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core' + 'Please open an issue at https://github.com/vuejs/core .', + [ErrorCodes.APP_UNMOUNT_CLEANUP]: 'app unmount cleanup function' } export type ErrorTypes = LifecycleHooks | ErrorCodes