diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index e8686700aae..854c2be5ff8 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -1,3 +1,4 @@ +import { nextTick } from 'vue' import { EffectScope, type Ref, @@ -213,4 +214,25 @@ describe('watch', () => { value.value = true expect(value.value).toBe(false) }) + + it('stop multiple watches by abort controller', async () => { + const controller = new AbortController() + const state = ref(0) + const cb1 = vi.fn() + const cb2 = vi.fn() + watch(state, cb1, { signal: controller.signal }) + watch(state, cb2, { signal: controller.signal }) + + state.value++ + await nextTick() + expect(cb1).toHaveBeenCalledTimes(1) + expect(cb2).toHaveBeenCalledTimes(1) + + controller.abort() + state.value++ + await nextTick() + // should not run callback + expect(cb1).toHaveBeenCalledTimes(1) + expect(cb2).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 532169a2dde..0808f60cddd 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -39,6 +39,7 @@ export type WatchCallback = ( export type OnCleanup = (cleanupFn: () => void) => void export interface WatchOptions extends DebuggerOptions { + signal?: AbortSignal immediate?: Immediate deep?: boolean | number once?: boolean @@ -117,7 +118,7 @@ export class WatcherEffect extends ReactiveEffect { public cb?: WatchCallback | null | undefined, public options: WatchOptions = EMPTY_OBJ, ) { - const { deep, once, call, onWarn } = options + const { deep, once, signal, call, onWarn } = options let getter: () => any let forceTrigger = false @@ -199,6 +200,10 @@ export class WatcherEffect extends ReactiveEffect { this.cb = cb + if (signal) { + signal.addEventListener('abort', this.stop.bind(this), { once: true }) + } + this.oldValue = isMultiSource ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 80a8b434502..906c5dba11b 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -434,6 +434,48 @@ describe('api: watch', () => { expect(dummy).toBe(1) }) + it('stopping the watcher (effect) by abort controller', async () => { + const controller = new AbortController() + const state = reactive({ count: 0 }) + let dummy + watchEffect( + () => { + dummy = state.count + }, + { signal: controller.signal }, + ) + expect(dummy).toBe(0) + + controller.abort() + state.count++ + await nextTick() + // should not update + expect(dummy).toBe(0) + }) + + it('stopping the watcher (with source) by abort controller', async () => { + const controller = new AbortController() + const state = reactive({ count: 0 }) + let dummy + watch( + () => state.count, + count => { + dummy = count + }, + { signal: controller.signal }, + ) + + state.count++ + await nextTick() + expect(dummy).toBe(1) + + controller.abort() + state.count++ + await nextTick() + // should not update + expect(dummy).toBe(1) + }) + it('cleanup registration (effect)', async () => { const state = reactive({ count: 0 }) const cleanup = vi.fn() diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 7dce012d90b..023557242fb 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -43,7 +43,11 @@ type MapSources = { : never } -export interface WatchEffectOptions extends DebuggerOptions { +export interface BaseWatchEffectOptions extends DebuggerOptions { + signal?: AbortSignal +} + +export interface WatchEffectOptions extends BaseWatchEffectOptions { flush?: 'pre' | 'post' | 'sync' } @@ -63,7 +67,7 @@ export function watchEffect( export function watchPostEffect( effect: WatchEffect, - options?: DebuggerOptions, + options?: BaseWatchEffectOptions, ): WatchHandle { return doWatch( effect, @@ -74,7 +78,7 @@ export function watchPostEffect( export function watchSyncEffect( effect: WatchEffect, - options?: DebuggerOptions, + options?: BaseWatchEffectOptions, ): WatchHandle { return doWatch( effect,