From 6784f0b1f8501746ea70d87d18ed63a62cf6b76d Mon Sep 17 00:00:00 2001 From: Rudy Date: Fri, 8 Dec 2023 12:24:44 +0800 Subject: [PATCH] fix(watch): should not fire pre watcher on child component unmount (#7181) close #7030 --- .../runtime-core/__tests__/apiWatch.spec.ts | 92 +++++++++++++++++++ packages/runtime-core/src/renderer.ts | 2 +- packages/runtime-core/src/scheduler.ts | 4 + 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 48fdd2888ec..2cb676aa8dc 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -549,6 +549,98 @@ describe('api: watch', () => { expect(cb).not.toHaveBeenCalled() }) + // #7030 + it('should not fire on child component unmount w/ flush: pre', async () => { + const visible = ref(true) + const cb = vi.fn() + const Parent = defineComponent({ + props: ['visible'], + render() { + return visible.value ? h(Comp) : null + } + }) + const Comp = { + setup() { + watch(visible, cb, { flush: 'pre' }) + }, + render() {} + } + const App = { + render() { + return h(Parent, { + visible: visible.value + }) + } + } + render(h(App), nodeOps.createElement('div')) + expect(cb).not.toHaveBeenCalled() + visible.value = false + await nextTick() + expect(cb).not.toHaveBeenCalled() + }) + + // #7030 + it('flush: pre watcher in child component should not fire before parent update', async () => { + const b = ref(0) + const calls: string[] = [] + + const Comp = { + setup() { + watch( + () => b.value, + val => { + calls.push('watcher child') + }, + { flush: 'pre' } + ) + return () => { + b.value + calls.push('render child') + } + } + } + + const Parent = { + props: ['a'], + setup() { + watch( + () => b.value, + val => { + calls.push('watcher parent') + }, + { flush: 'pre' } + ) + return () => { + b.value + calls.push('render parent') + return h(Comp) + } + } + } + + const App = { + render() { + return h(Parent, { + a: b.value + }) + } + } + + render(h(App), nodeOps.createElement('div')) + expect(calls).toEqual(['render parent', 'render child']) + + b.value++ + await nextTick() + expect(calls).toEqual([ + 'render parent', + 'render child', + 'watcher parent', + 'render parent', + 'watcher child', + 'render child' + ]) + }) + // #1763 it('flush: pre watcher watching props should fire before child update', async () => { const a = ref(0) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 8a3b4ffa3af..2f31ad9a044 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1582,7 +1582,7 @@ function baseCreateRenderer( pauseTracking() // props update may have triggered pre-flush watchers. // flush them before the render update. - flushPreFlushCbs() + flushPreFlushCbs(instance) resetTracking() } diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index e4c7fbc0f4d..5b096b563e4 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -139,6 +139,7 @@ export function queuePostFlushCb(cb: SchedulerJobs) { } export function flushPreFlushCbs( + instance?: ComponentInternalInstance, seen?: CountMap, // if currently flushing, skip the current job itself i = isFlushing ? flushIndex + 1 : 0 @@ -149,6 +150,9 @@ export function flushPreFlushCbs( for (; i < queue.length; i++) { const cb = queue[i] if (cb && cb.pre) { + if (instance && cb.id !== instance.uid) { + continue + } if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { continue }