99 isReactive ,
1010 isRef ,
1111 isShallow ,
12+ pauseTracking ,
13+ resetTracking ,
1214} from '@vue/reactivity'
1315import { type SchedulerJob , queueJob } from './scheduler'
1416import {
@@ -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+
172207function 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 ( )
0 commit comments