diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 7b64e50b..31bca6be 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -280,38 +280,7 @@ describe('api: watch', () => { expect(cleanup).toHaveBeenCalledTimes(2) }) - it('flush timing: post (default)', async () => { - const count = ref(0) - let callCount = 0 - let result - const assertion = jest.fn(count => { - 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 - }) - - const Comp = { - setup() { - watchEffect(() => { - assertion(count.value) - }) - return () => count.value - } - } - const root = nodeOps.createElement('div') - render(h(Comp), root) - expect(assertion).toHaveBeenCalledTimes(1) - expect(result).toBe(true) - - count.value++ - await nextTick() - expect(assertion).toHaveBeenCalledTimes(2) - expect(result).toBe(true) - }) - - it('flush timing: pre', async () => { + it('flush timing: pre (default)', async () => { const count = ref(0) const count2 = ref(0) @@ -332,14 +301,9 @@ describe('api: watch', () => { const Comp = { setup() { - watchEffect( - () => { - assertion(count.value, count2.value) - }, - { - flush: 'pre' - } - ) + watchEffect(() => { + assertion(count.value, count2.value) + }) return () => count.value } } @@ -358,6 +322,35 @@ describe('api: watch', () => { expect(result2).toBe(true) }) + it('flush timing: post', async () => { + const count = ref(0) + let result + const assertion = jest.fn(count => { + result = serializeInner(root) === `${count}` + }) + + const Comp = { + setup() { + watchEffect( + () => { + assertion(count.value) + }, + { flush: 'post' } + ) + return () => count.value + } + } + const root = nodeOps.createElement('div') + render(h(Comp), root) + expect(assertion).toHaveBeenCalledTimes(1) + expect(result).toBe(true) + + count.value++ + await nextTick() + expect(assertion).toHaveBeenCalledTimes(2) + expect(result).toBe(true) + }) + it('flush timing: sync', async () => { const count = ref(0) const count2 = ref(0) @@ -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 e4a57804..dd7d67bc 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 ac1b1d16..14253a2a 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 3259ca68..ac6eec56 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