Skip to content

Commit db00fe9

Browse files
fix(reactivity): do not track deps in watcher callbacks run inside another effect (#15782)
close #5009
1 parent 9f3b437 commit db00fe9

3 files changed

Lines changed: 100 additions & 5 deletions

File tree

‎packages/reactivity/__tests__/watch.spec.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type WatchOptions,
66
type WatchScheduler,
77
computed,
8+
effect,
89
onWatcherCleanup,
910
ref,
1011
watch,
@@ -342,4 +343,55 @@ describe('watch', () => {
342343
expect(() => (source.value = 2)).not.toThrow()
343344
expect(callback).toHaveBeenCalledTimes(1)
344345
})
346+
347+
// #5009
348+
test('should not track deps of cleanup and callback run inside another effect', () => {
349+
const source = ref(0)
350+
const dep = ref(0)
351+
const cb = vi.fn(() => {
352+
dep.value
353+
onWatcherCleanup(() => dep.value)
354+
})
355+
watch(source, cb)
356+
source.value++
357+
expect(cb).toHaveBeenCalledTimes(1)
358+
359+
const fn = vi.fn(() => {
360+
source.value = 10
361+
})
362+
effect(fn)
363+
expect(cb).toHaveBeenCalledTimes(2)
364+
expect(fn).toHaveBeenCalledTimes(1)
365+
366+
dep.value++
367+
expect(fn).toHaveBeenCalledTimes(1)
368+
})
369+
370+
test('should restore tracking when cleanup throws inside another effect', () => {
371+
const source = ref(0)
372+
const dep = ref(0)
373+
const err = new Error('cleanup error')
374+
watch(source, () => {
375+
onWatcherCleanup(() => {
376+
throw err
377+
})
378+
})
379+
source.value++
380+
381+
let dummy
382+
let caught
383+
effect(() => {
384+
try {
385+
source.value = 10
386+
} catch (e) {
387+
caught = e
388+
}
389+
dummy = dep.value
390+
})
391+
expect(caught).toBe(err)
392+
expect(dummy).toBe(0)
393+
394+
dep.value++
395+
expect(dummy).toBe(1)
396+
})
345397
})

‎packages/reactivity/src/watch.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,14 @@ export function watch(
251251
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
252252
: hasChanged(newValue, oldValue))
253253
) {
254-
// cleanup before running cb again
255-
if (cleanup) {
256-
cleanup()
257-
}
258254
const currentWatcher = activeWatcher
259-
activeWatcher = effect
255+
pauseTracking()
260256
try {
257+
// cleanup before running cb again
258+
if (cleanup) {
259+
cleanup()
260+
}
261+
activeWatcher = effect
261262
const args = [
262263
newValue,
263264
// pass undefined as the old value when it's changed for the first time
@@ -275,6 +276,7 @@ export function watch(
275276
cb!(...args)
276277
} finally {
277278
activeWatcher = currentWatcher
279+
resetTracking()
278280
}
279281
}
280282
} else {

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,47 @@ describe('api: watch', () => {
14241424
expect(updated).toHaveBeenCalledTimes(1)
14251425
})
14261426

1427+
// #5009
1428+
test('sync watcher callbacks should not track dependencies', async () => {
1429+
const a = ref(0)
1430+
const b = ref(0)
1431+
const updated = vi.fn()
1432+
1433+
const Child = defineComponent({
1434+
props: ['a'],
1435+
updated,
1436+
watch: {
1437+
a: {
1438+
handler() {
1439+
b.value
1440+
},
1441+
flush: 'sync',
1442+
},
1443+
},
1444+
render() {
1445+
return h('div', this.a)
1446+
},
1447+
})
1448+
1449+
const Parent = defineComponent({
1450+
render() {
1451+
return h(Child, { a: a.value })
1452+
},
1453+
})
1454+
1455+
const root = nodeOps.createElement('div')
1456+
createApp(Parent).mount(root)
1457+
1458+
a.value++
1459+
await nextTick()
1460+
expect(updated).toHaveBeenCalledTimes(1)
1461+
1462+
b.value++
1463+
await nextTick()
1464+
// should not track b as dependency of Child
1465+
expect(updated).toHaveBeenCalledTimes(1)
1466+
})
1467+
14271468
test('watching keypath', async () => {
14281469
const spy = vi.fn()
14291470
const Comp = defineComponent({

0 commit comments

Comments
 (0)