Skip to content

Commit c7f5574

Browse files
committed
feat(reactivity): add support for AbortController to watch
1 parent a9fb319 commit c7f5574

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

packages/reactivity/__tests__/watch.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { nextTick } from 'vue'
12
import {
23
EffectScope,
34
type Ref,
@@ -213,4 +214,25 @@ describe('watch', () => {
213214
value.value = true
214215
expect(value.value).toBe(false)
215216
})
217+
218+
it('stop multiple watches by abort controller', async () => {
219+
const controller = new AbortController()
220+
const state = ref(0)
221+
const cb1 = vi.fn()
222+
const cb2 = vi.fn()
223+
watch(state, cb1, { signal: controller.signal })
224+
watch(state, cb2, { signal: controller.signal })
225+
226+
state.value++
227+
await nextTick()
228+
expect(cb1).toHaveBeenCalledTimes(1)
229+
expect(cb2).toHaveBeenCalledTimes(1)
230+
231+
controller.abort()
232+
state.value++
233+
await nextTick()
234+
// should not run callback
235+
expect(cb1).toHaveBeenCalledTimes(1)
236+
expect(cb2).toHaveBeenCalledTimes(1)
237+
})
216238
})

packages/reactivity/src/watch.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type WatchCallback<V = any, OV = any> = (
3939
export type OnCleanup = (cleanupFn: () => void) => void
4040

4141
export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
42+
signal?: AbortSignal
4243
immediate?: Immediate
4344
deep?: boolean | number
4445
once?: boolean
@@ -117,7 +118,7 @@ export class WatcherEffect extends ReactiveEffect {
117118
public cb?: WatchCallback<any, any> | null | undefined,
118119
public options: WatchOptions = EMPTY_OBJ,
119120
) {
120-
const { deep, once, call, onWarn } = options
121+
const { deep, once, signal, call, onWarn } = options
121122

122123
let getter: () => any
123124
let forceTrigger = false
@@ -199,6 +200,10 @@ export class WatcherEffect extends ReactiveEffect {
199200

200201
this.cb = cb
201202

203+
if (signal) {
204+
signal.addEventListener('abort', this.stop.bind(this), { once: true })
205+
}
206+
202207
this.oldValue = isMultiSource
203208
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
204209
: INITIAL_WATCHER_VALUE

packages/runtime-core/__tests__/apiWatch.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,48 @@ describe('api: watch', () => {
434434
expect(dummy).toBe(1)
435435
})
436436

437+
it('stopping the watcher (effect) by abort controller', async () => {
438+
const controller = new AbortController()
439+
const state = reactive({ count: 0 })
440+
let dummy
441+
watchEffect(
442+
() => {
443+
dummy = state.count
444+
},
445+
{ signal: controller.signal },
446+
)
447+
expect(dummy).toBe(0)
448+
449+
controller.abort()
450+
state.count++
451+
await nextTick()
452+
// should not update
453+
expect(dummy).toBe(0)
454+
})
455+
456+
it('stopping the watcher (with source) by abort controller', async () => {
457+
const controller = new AbortController()
458+
const state = reactive({ count: 0 })
459+
let dummy
460+
watch(
461+
() => state.count,
462+
count => {
463+
dummy = count
464+
},
465+
{ signal: controller.signal },
466+
)
467+
468+
state.count++
469+
await nextTick()
470+
expect(dummy).toBe(1)
471+
472+
controller.abort()
473+
state.count++
474+
await nextTick()
475+
// should not update
476+
expect(dummy).toBe(1)
477+
})
478+
437479
it('cleanup registration (effect)', async () => {
438480
const state = reactive({ count: 0 })
439481
const cleanup = vi.fn()

packages/runtime-core/src/apiWatch.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ type MapSources<T, Immediate> = {
4343
: never
4444
}
4545

46-
export interface WatchEffectOptions extends DebuggerOptions {
46+
export interface BaseWatchEffectOptions extends DebuggerOptions {
47+
signal?: AbortSignal
48+
}
49+
50+
export interface WatchEffectOptions extends BaseWatchEffectOptions {
4751
flush?: 'pre' | 'post' | 'sync'
4852
}
4953

@@ -63,7 +67,7 @@ export function watchEffect(
6367

6468
export function watchPostEffect(
6569
effect: WatchEffect,
66-
options?: DebuggerOptions,
70+
options?: BaseWatchEffectOptions,
6771
): WatchHandle {
6872
return doWatch(
6973
effect,
@@ -74,7 +78,7 @@ export function watchPostEffect(
7478

7579
export function watchSyncEffect(
7680
effect: WatchEffect,
77-
options?: DebuggerOptions,
81+
options?: BaseWatchEffectOptions,
7882
): WatchHandle {
7983
return doWatch(
8084
effect,

0 commit comments

Comments
 (0)