Skip to content

Commit e7f4b48

Browse files
committed
feat(runtime-core): onEffectCleanup
1 parent ee4cd78 commit e7f4b48

File tree

3 files changed

+112
-28
lines changed

3 files changed

+112
-28
lines changed

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
defineComponent,
66
getCurrentInstance,
77
nextTick,
8+
onEffectCleanup,
89
reactive,
910
ref,
1011
watch,
@@ -393,6 +394,35 @@ describe('api: watch', () => {
393394
expect(cleanup).toHaveBeenCalledTimes(2)
394395
})
395396

397+
it('onEffectCleanup registration', async () => {
398+
const count = ref(0)
399+
const cleanupEffect = vi.fn()
400+
const cleanupWatch = vi.fn()
401+
402+
const stopEffect = watchEffect(() => {
403+
onEffectCleanup(cleanupEffect)
404+
count.value
405+
})
406+
const stopWatch = watch(count, () => {
407+
onEffectCleanup(cleanupWatch)
408+
})
409+
410+
count.value++
411+
await nextTick()
412+
expect(cleanupEffect).toHaveBeenCalledTimes(1)
413+
expect(cleanupWatch).toHaveBeenCalledTimes(0)
414+
415+
count.value++
416+
await nextTick()
417+
expect(cleanupEffect).toHaveBeenCalledTimes(2)
418+
expect(cleanupWatch).toHaveBeenCalledTimes(1)
419+
420+
stopEffect()
421+
expect(cleanupEffect).toHaveBeenCalledTimes(3)
422+
stopWatch()
423+
expect(cleanupWatch).toHaveBeenCalledTimes(2)
424+
})
425+
396426
it('flush timing: pre (default)', async () => {
397427
const count = ref(0)
398428
const count2 = ref(0)

packages/runtime-core/src/apiWatch.ts

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
isReactive,
1010
isRef,
1111
isShallow,
12+
pauseTracking,
13+
resetTracking,
1214
} from '@vue/reactivity'
1315
import { type SchedulerJob, queueJob } from './scheduler'
1416
import {
@@ -169,6 +171,39 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
169171
return doWatch(source as any, cb, options)
170172
}
171173

174+
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
175+
let activeEffect: ReactiveEffect | undefined = undefined
176+
177+
/**
178+
* Returns the current active effect if there is one.
179+
*/
180+
export function getCurrentEffect() {
181+
return activeEffect
182+
}
183+
184+
/**
185+
* Registers a cleanup callback on the current active effect. This
186+
* registered cleanup callback will be invoked right before the
187+
* associated effect re-runs.
188+
*
189+
* @param cleanupFn - The callback function to attach to the effect's cleanup.
190+
*/
191+
export function onEffectCleanup(cleanupFn: () => void) {
192+
// in SSR there is no need to call the invalidate callback
193+
if (__SSR__ && isInSSRComponentSetup) return
194+
if (activeEffect) {
195+
const cleanups =
196+
cleanupMap.get(activeEffect) ||
197+
cleanupMap.set(activeEffect, []).get(activeEffect)!
198+
cleanups.push(cleanupFn)
199+
} else if (__DEV__) {
200+
warn(
201+
`onEffectCleanup() was called when there was no active effect` +
202+
` to associate with.`,
203+
)
204+
}
205+
}
206+
172207
function doWatch(
173208
source: WatchSource | WatchSource[] | WatchEffect | object,
174209
cb: WatchCallback | null,
@@ -235,6 +270,7 @@ function doWatch(
235270
traverse(source, deep === false ? 1 : undefined)
236271

237272
let getter: () => any
273+
let cleanup: (() => void) | undefined
238274
let forceTrigger = false
239275
let isMultiSource = false
240276

@@ -268,14 +304,25 @@ function doWatch(
268304
// no cb -> simple effect
269305
getter = () => {
270306
if (cleanup) {
271-
cleanup()
307+
pauseTracking()
308+
try {
309+
cleanup()
310+
} finally {
311+
resetTracking()
312+
}
313+
}
314+
const currentEffect = activeEffect
315+
activeEffect = effect
316+
try {
317+
return callWithAsyncErrorHandling(
318+
source,
319+
instance,
320+
ErrorCodes.WATCH_CALLBACK,
321+
[onEffectCleanup],
322+
)
323+
} finally {
324+
activeEffect = currentEffect
272325
}
273-
return callWithAsyncErrorHandling(
274-
source,
275-
instance,
276-
ErrorCodes.WATCH_CALLBACK,
277-
[onCleanup],
278-
)
279326
}
280327
}
281328
} else {
@@ -303,27 +350,17 @@ function doWatch(
303350
getter = () => traverse(baseGetter())
304351
}
305352

306-
let cleanup: (() => void) | undefined
307-
let onCleanup: OnCleanup = (fn: () => void) => {
308-
cleanup = effect.onStop = () => {
309-
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
310-
cleanup = effect.onStop = undefined
311-
}
312-
}
313-
314353
// in SSR there is no need to setup an actual effect, and it should be noop
315354
// unless it's eager or sync flush
316355
let ssrCleanup: (() => void)[] | undefined
317356
if (__SSR__ && isInSSRComponentSetup) {
318-
// we will also not call the invalidate callback (+ runner is not set up)
319-
onCleanup = NOOP
320357
if (!cb) {
321358
getter()
322359
} else if (immediate) {
323360
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
324361
getter(),
325362
isMultiSource ? [] : undefined,
326-
onCleanup,
363+
onEffectCleanup,
327364
])
328365
}
329366
if (flush === 'sync') {
@@ -358,16 +395,22 @@ function doWatch(
358395
if (cleanup) {
359396
cleanup()
360397
}
361-
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
362-
newValue,
363-
// pass undefined as the old value when it's changed for the first time
364-
oldValue === INITIAL_WATCHER_VALUE
365-
? undefined
366-
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
367-
? []
368-
: oldValue,
369-
onCleanup,
370-
])
398+
const currentEffect = activeEffect
399+
activeEffect = effect
400+
try {
401+
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
402+
newValue,
403+
// pass undefined as the old value when it's changed for the first time
404+
oldValue === INITIAL_WATCHER_VALUE
405+
? undefined
406+
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
407+
? []
408+
: oldValue,
409+
onEffectCleanup,
410+
])
411+
} finally {
412+
activeEffect = currentEffect
413+
}
371414
oldValue = newValue
372415
}
373416
} else {
@@ -394,6 +437,16 @@ function doWatch(
394437

395438
const effect = new ReactiveEffect(getter, NOOP, scheduler)
396439

440+
cleanup = effect.onStop = () => {
441+
const cleanups = cleanupMap.get(effect)
442+
if (cleanups) {
443+
cleanups.forEach(cleanup =>
444+
callWithErrorHandling(cleanup, instance, ErrorCodes.WATCH_CLEANUP),
445+
)
446+
cleanupMap.delete(effect)
447+
}
448+
}
449+
397450
const scope = getCurrentScope()
398451
const unwatch = () => {
399452
effect.stop()

packages/runtime-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export {
4141
watchEffect,
4242
watchPostEffect,
4343
watchSyncEffect,
44+
onEffectCleanup,
4445
} from './apiWatch'
4546
export {
4647
onBeforeMount,

0 commit comments

Comments
 (0)