feat(runtime-core): add app.onUnmount() for registering cleanup functions (#4619)

close #4516
This commit is contained in:
Thorsten Lünborg 2024-04-29 12:47:56 +02:00 committed by GitHub
parent 801b8dea3b
commit 582a3a382b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 0 deletions

View File

@ -344,6 +344,36 @@ describe('api: createApp', () => {
).toHaveBeenWarnedTimes(1) ).toHaveBeenWarnedTimes(1)
}) })
test('onUnmount', () => {
const cleanup = vi.fn().mockName('plugin cleanup')
const PluginA: Plugin = app => {
app.provide('foo', 1)
app.onUnmount(cleanup)
}
const PluginB: Plugin = {
install: (app, arg1, arg2) => {
app.provide('bar', arg1 + arg2)
app.onUnmount(cleanup)
},
}
const app = createApp({
render: () => `Test`,
})
app.use(PluginA)
app.use(PluginB)
const root = nodeOps.createElement('div')
app.mount(root)
//also can be added after mount
app.onUnmount(cleanup)
app.unmount()
expect(cleanup).toHaveBeenCalledTimes(3)
})
test('config.errorHandler', () => { test('config.errorHandler', () => {
const error = new Error() const error = new Error()
const count = ref(0) const count = ref(0)

View File

@ -27,6 +27,7 @@ import { version } from '.'
import { installAppCompatProperties } from './compat/global' import { installAppCompatProperties } from './compat/global'
import type { NormalizedPropsOptions } from './componentProps' import type { NormalizedPropsOptions } from './componentProps'
import type { ObjectEmitsOptions } from './componentEmits' import type { ObjectEmitsOptions } from './componentEmits'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { DefineComponent } from './apiDefineComponent' import type { DefineComponent } from './apiDefineComponent'
export interface App<HostElement = any> { export interface App<HostElement = any> {
@ -50,6 +51,7 @@ export interface App<HostElement = any> {
namespace?: boolean | ElementNamespace, namespace?: boolean | ElementNamespace,
): ComponentPublicInstance ): ComponentPublicInstance
unmount(): void unmount(): void
onUnmount(cb: () => void): void
provide<T>(key: InjectionKey<T> | string, value: T): this provide<T>(key: InjectionKey<T> | string, value: T): this
/** /**
@ -214,6 +216,7 @@ export function createAppAPI<HostElement>(
const context = createAppContext() const context = createAppContext()
const installedPlugins = new WeakSet() const installedPlugins = new WeakSet()
const pluginCleanupFns: Array<() => any> = []
let isMounted = false let isMounted = false
@ -366,8 +369,23 @@ export function createAppAPI<HostElement>(
} }
}, },
onUnmount(cleanupFn: () => void) {
if (__DEV__ && typeof cleanupFn !== 'function') {
warn(
`Expected function as first argument to app.onUnmount(), ` +
`but got ${typeof cleanupFn}`,
)
}
pluginCleanupFns.push(cleanupFn)
},
unmount() { unmount() {
if (isMounted) { if (isMounted) {
callWithAsyncErrorHandling(
pluginCleanupFns,
app._instance,
ErrorCodes.APP_UNMOUNT_CLEANUP,
)
render(null, app._container) render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null app._instance = null

View File

@ -23,6 +23,7 @@ export enum ErrorCodes {
FUNCTION_REF, FUNCTION_REF,
ASYNC_COMPONENT_LOADER, ASYNC_COMPONENT_LOADER,
SCHEDULER, SCHEDULER,
APP_UNMOUNT_CLEANUP,
} }
export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = { export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
@ -57,6 +58,7 @@ export const ErrorTypeStrings: Record<LifecycleHooks | ErrorCodes, string> = {
[ErrorCodes.SCHEDULER]: [ErrorCodes.SCHEDULER]:
'scheduler flush. This is likely a Vue internals bug. ' + 'scheduler flush. This is likely a Vue internals bug. ' +
'Please open an issue at https://github.com/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 export type ErrorTypes = LifecycleHooks | ErrorCodes