diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 7b64e50bd5f..31bca6bed3f 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -280,22 +280,29 @@ describe('api: watch', () => { expect(cleanup).toHaveBeenCalledTimes(2) }) - it('flush timing: post (default)', async () => { + it('flush timing: pre (default)', async () => { const count = ref(0) + const count2 = ref(0) + let callCount = 0 - let result - const assertion = jest.fn(count => { + let result1 + let result2 + const assertion = jest.fn((count, count2Value) => { callCount++ // on mount, the watcher callback should be called before DOM render - // on update, should be called after the count is updated - const expectedDOM = callCount === 1 ? `` : `${count}` - result = serializeInner(root) === expectedDOM + // on update, should be called before the count is updated + const expectedDOM = callCount === 1 ? `` : `${count - 1}` + result1 = serializeInner(root) === expectedDOM + + // in a pre-flush callback, all state should have been updated + const expectedState = callCount - 1 + result2 = count === expectedState && count2Value === expectedState }) const Comp = { setup() { watchEffect(() => { - assertion(count.value) + assertion(count.value, count2.value) }) return () => count.value } @@ -303,42 +310,32 @@ describe('api: watch', () => { const root = nodeOps.createElement('div') render(h(Comp), root) expect(assertion).toHaveBeenCalledTimes(1) - expect(result).toBe(true) + expect(result1).toBe(true) + expect(result2).toBe(true) count.value++ + count2.value++ await nextTick() + // two mutations should result in 1 callback execution expect(assertion).toHaveBeenCalledTimes(2) - expect(result).toBe(true) + expect(result1).toBe(true) + expect(result2).toBe(true) }) - it('flush timing: pre', async () => { + it('flush timing: post', async () => { const count = ref(0) - const count2 = ref(0) - - let callCount = 0 - let result1 - let result2 - const assertion = jest.fn((count, count2Value) => { - callCount++ - // on mount, the watcher callback should be called before DOM render - // on update, should be called before the count is updated - const expectedDOM = callCount === 1 ? `` : `${count - 1}` - result1 = serializeInner(root) === expectedDOM - - // in a pre-flush callback, all state should have been updated - const expectedState = callCount - 1 - result2 = count === expectedState && count2Value === expectedState + let result + const assertion = jest.fn(count => { + result = serializeInner(root) === `${count}` }) const Comp = { setup() { watchEffect( () => { - assertion(count.value, count2.value) + assertion(count.value) }, - { - flush: 'pre' - } + { flush: 'post' } ) return () => count.value } @@ -346,16 +343,12 @@ describe('api: watch', () => { const root = nodeOps.createElement('div') render(h(Comp), root) expect(assertion).toHaveBeenCalledTimes(1) - expect(result1).toBe(true) - expect(result2).toBe(true) + expect(result).toBe(true) count.value++ - count2.value++ await nextTick() - // two mutations should result in 1 callback execution expect(assertion).toHaveBeenCalledTimes(2) - expect(result1).toBe(true) - expect(result2).toBe(true) + expect(result).toBe(true) }) it('flush timing: sync', async () => { @@ -410,7 +403,7 @@ describe('api: watch', () => { const cb = jest.fn() const Comp = { setup() { - watch(toggle, cb) + watch(toggle, cb, { flush: 'post' }) }, render() {} } diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index e4a57804314..dd7d67bc909 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -154,7 +154,7 @@ describe('Suspense', () => { expect(onResolve).toHaveBeenCalled() }) - test('buffer mounted/updated hooks & watch callbacks', async () => { + test('buffer mounted/updated hooks & post flush watch callbacks', async () => { const deps: Promise[] = [] const calls: string[] = [] const toggle = ref(true) @@ -165,14 +165,21 @@ describe('Suspense', () => { // extra tick needed for Node 12+ deps.push(p.then(() => Promise.resolve())) - watchEffect(() => { - calls.push('immediate effect') - }) + watchEffect( + () => { + calls.push('watch effect') + }, + { flush: 'post' } + ) const count = ref(0) - watch(count, () => { - calls.push('watch callback') - }) + watch( + count, + () => { + calls.push('watch callback') + }, + { flush: 'post' } + ) count.value++ // trigger the watcher now onMounted(() => { @@ -201,12 +208,12 @@ describe('Suspense', () => { const root = nodeOps.createElement('div') render(h(Comp), root) expect(serializeInner(root)).toBe(`
fallback
`) - expect(calls).toEqual([`immediate effect`]) + expect(calls).toEqual([]) await Promise.all(deps) await nextTick() expect(serializeInner(root)).toBe(`
async
`) - expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`]) + expect(calls).toEqual([`watch effect`, `watch callback`, `mounted`]) // effects inside an already resolved suspense should happen at normal timing toggle.value = false @@ -214,7 +221,7 @@ describe('Suspense', () => { await nextTick() expect(serializeInner(root)).toBe(``) expect(calls).toEqual([ - `immediate effect`, + `watch effect`, `watch callback`, `mounted`, 'unmounted' @@ -319,14 +326,21 @@ describe('Suspense', () => { const p = new Promise(r => setTimeout(r, 1)) deps.push(p) - watchEffect(() => { - calls.push('immediate effect') - }) + watchEffect( + () => { + calls.push('watch effect') + }, + { flush: 'post' } + ) const count = ref(0) - watch(count, () => { - calls.push('watch callback') - }) + watch( + count, + () => { + calls.push('watch callback') + }, + { flush: 'post' } + ) count.value++ // trigger the watcher now onMounted(() => { @@ -355,7 +369,7 @@ describe('Suspense', () => { const root = nodeOps.createElement('div') render(h(Comp), root) expect(serializeInner(root)).toBe(`
fallback
`) - expect(calls).toEqual(['immediate effect']) + expect(calls).toEqual([]) // remove the async dep before it's resolved toggle.value = false @@ -366,8 +380,8 @@ describe('Suspense', () => { await Promise.all(deps) await nextTick() expect(serializeInner(root)).toBe(``) - // should discard effects (except for immediate ones) - expect(calls).toEqual(['immediate effect', 'unmounted']) + // should discard effects (except for unmount) + expect(calls).toEqual(['unmounted']) }) test('unmount suspense after resolve', async () => { diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index ac1b1d16d7f..14253a2a403 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -268,9 +268,10 @@ function doWatch( let scheduler: (job: () => any) => void if (flush === 'sync') { scheduler = job - } else if (flush === 'pre') { - // ensure it's queued before component updates (which have positive ids) - job.id = -1 + } else if (flush === 'post') { + scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) + } else { + // default: 'pre' scheduler = () => { if (!instance || instance.isMounted) { queuePreFlushCb(job) @@ -280,8 +281,6 @@ function doWatch( job() } } - } else { - scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } const runner = effect(getter, { @@ -300,6 +299,8 @@ function doWatch( } else { oldValue = runner() } + } else if (flush === 'post') { + queuePostRenderEffect(runner, instance && instance.suspense) } else { runner() } diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 3259ca685c6..ac6eec56f2e 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -171,15 +171,18 @@ const KeepAliveImpl = { keys.delete(key) } + // prune cache on include/exclude prop change watch( () => [props.include, props.exclude], ([include, exclude]) => { include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => !matches(exclude, name)) - } + }, + // prune post-render after `current` has been updated + { flush: 'post' } ) - // cache sub tree in beforeMount/Update (i.e. right after the render) + // cache sub tree after render let pendingCacheKey: CacheKey | null = null const cacheSubtree = () => { // fix #1621, the pendingCacheKey could be 0