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 ,
@@ -234,7 +269,9 @@ function doWatch(
234269 : // for deep: false, only traverse root-level properties
235270 traverse ( source , deep === false ? 1 : undefined )
236271
272+ let effect : ReactiveEffect
237273 let getter : ( ) => any
274+ let cleanup : ( ( ) => void ) | undefined
238275 let forceTrigger = false
239276 let isMultiSource = false
240277
@@ -268,14 +305,25 @@ function doWatch(
268305 // no cb -> simple effect
269306 getter = ( ) => {
270307 if ( cleanup ) {
271- cleanup ( )
308+ pauseTracking ( )
309+ try {
310+ cleanup ( )
311+ } finally {
312+ resetTracking ( )
313+ }
314+ }
315+ const currentEffect = activeEffect
316+ activeEffect = effect
317+ try {
318+ return callWithAsyncErrorHandling (
319+ source ,
320+ instance ,
321+ ErrorCodes . WATCH_CALLBACK ,
322+ [ onEffectCleanup ] ,
323+ )
324+ } finally {
325+ activeEffect = currentEffect
272326 }
273- return callWithAsyncErrorHandling (
274- source ,
275- instance ,
276- ErrorCodes . WATCH_CALLBACK ,
277- [ onCleanup ] ,
278- )
279327 }
280328 }
281329 } else {
@@ -303,27 +351,17 @@ function doWatch(
303351 getter = ( ) => traverse ( baseGetter ( ) )
304352 }
305353
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-
314354 // in SSR there is no need to setup an actual effect, and it should be noop
315355 // unless it's eager or sync flush
316356 let ssrCleanup : ( ( ) => void ) [ ] | undefined
317357 if ( __SSR__ && isInSSRComponentSetup ) {
318- // we will also not call the invalidate callback (+ runner is not set up)
319- onCleanup = NOOP
320358 if ( ! cb ) {
321359 getter ( )
322360 } else if ( immediate ) {
323361 callWithAsyncErrorHandling ( cb , instance , ErrorCodes . WATCH_CALLBACK , [
324362 getter ( ) ,
325363 isMultiSource ? [ ] : undefined ,
326- onCleanup ,
364+ onEffectCleanup ,
327365 ] )
328366 }
329367 if ( flush === 'sync' ) {
@@ -358,16 +396,22 @@ function doWatch(
358396 if ( cleanup ) {
359397 cleanup ( )
360398 }
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- ] )
399+ const currentEffect = activeEffect
400+ activeEffect = effect
401+ try {
402+ callWithAsyncErrorHandling ( cb , instance , ErrorCodes . WATCH_CALLBACK , [
403+ newValue ,
404+ // pass undefined as the old value when it's changed for the first time
405+ oldValue === INITIAL_WATCHER_VALUE
406+ ? undefined
407+ : isMultiSource && oldValue [ 0 ] === INITIAL_WATCHER_VALUE
408+ ? [ ]
409+ : oldValue ,
410+ onEffectCleanup ,
411+ ] )
412+ } finally {
413+ activeEffect = currentEffect
414+ }
371415 oldValue = newValue
372416 }
373417 } else {
@@ -392,7 +436,17 @@ function doWatch(
392436 scheduler = ( ) => queueJob ( job )
393437 }
394438
395- const effect = new ReactiveEffect ( getter , NOOP , scheduler )
439+ effect = new ReactiveEffect ( getter , NOOP , scheduler )
440+
441+ cleanup = effect . onStop = ( ) => {
442+ const cleanups = cleanupMap . get ( effect )
443+ if ( cleanups ) {
444+ cleanups . forEach ( cleanup =>
445+ callWithErrorHandling ( cleanup , instance , ErrorCodes . WATCH_CLEANUP ) ,
446+ )
447+ cleanupMap . delete ( effect )
448+ }
449+ }
396450
397451 const scope = getCurrentScope ( )
398452 const unwatch = ( ) => {
0 commit comments