From 811c84f4e7c3bbdcefa180b6a9fff4d4eb177a7a Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 16:49:13 +0800 Subject: [PATCH 1/8] fix(Transition): avoid leave hooks call multiple times --- packages/runtime-core/src/components/BaseTransition.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index a31f28b2388..1e6e065d4f6 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -224,6 +224,7 @@ const BaseTransitionImpl: ComponentOptions = { if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) { instance.update() } + leavingHooks.afterLeave = undefined } return emptyPlaceholder(child) } else if (mode === 'in-out' && innerChild.type !== Comment) { @@ -244,6 +245,7 @@ const BaseTransitionImpl: ComponentOptions = { delete enterHooks.delayedLeave } enterHooks.delayedLeave = delayedLeave + leavingHooks.delayLeave = undefined } } } From b2a5b16f3cc84103054d24c782c9342392c86dad Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 16:52:15 +0800 Subject: [PATCH 2/8] chore: minor tweaks --- packages/runtime-core/src/components/BaseTransition.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 1e6e065d4f6..b653e2d1d89 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -224,7 +224,7 @@ const BaseTransitionImpl: ComponentOptions = { if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) { instance.update() } - leavingHooks.afterLeave = undefined + delete leavingHooks.afterLeave } return emptyPlaceholder(child) } else if (mode === 'in-out' && innerChild.type !== Comment) { @@ -245,7 +245,6 @@ const BaseTransitionImpl: ComponentOptions = { delete enterHooks.delayedLeave } enterHooks.delayedLeave = delayedLeave - leavingHooks.delayLeave = undefined } } } From 3189297745103cbff95aadeb4a4e7d94f0fa390c Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 17:18:52 +0800 Subject: [PATCH 3/8] test: add test case --- packages/vue/__tests__/e2e/Transition.spec.ts | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index b9e9117289e..123d554e3f5 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1427,9 +1427,11 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + }) + describe('transition with KeepAlive', () => { test( - 'w/ KeepAlive + unmount innerChild', + 'unmount innerChild', async () => { const unmountSpy = vi.fn() await page().exposeFunction('unmountSpy', unmountSpy) @@ -1484,6 +1486,82 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #11775 + test( + 'replace child and update include at the same time (out-in mode)', + async () => { + const updatedSpy = vi.fn() + await page().exposeFunction('updatedSpy', updatedSpy) + await page().evaluate(() => { + const { updatedSpy } = window as any + const { createApp, ref, shallowRef, h, onUpdated } = (window as any) + .Vue + createApp({ + template: ` +
+ + + + + +
+ + + + `, + components: { + CompA: { + name: 'CompA', + setup() { + onUpdated(updatedSpy) + return () => h('div', 'CompA') + }, + }, + CompB: { + name: 'CompB', + setup() { + return () => h('div', 'CompB') + }, + }, + CompC: { + name: 'CompC', + setup() { + return () => h('div', 'CompC') + }, + }, + }, + setup: () => { + const includeRef = ref(['CompA', 'CompB', 'CompC']) + const current = shallowRef('CompA') + const switchToB = () => (current.value = 'CompB') + const switchToC = () => (current.value = 'CompC') + const switchToA = () => { + current.value = 'CompA' + includeRef.value = ['CompA'] + } + return { current, switchToB, switchToC, switchToA, includeRef } + }, + }).mount('#app') + }) + + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + await click('#switchToB') + await nextTick() + await click('#switchToC') + await transitionFinish() + expect(await html('#container')).toBe('
CompC
') + + await click('#switchToA') + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + // expect updatedSpy to be called once + expect(updatedSpy).toBeCalledTimes(1) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => { From 239f9121d678e007a8cb0b63a2b8635f393a0c0e Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 20:40:52 +0800 Subject: [PATCH 4/8] chore: update --- .../runtime-core/src/components/KeepAlive.ts | 4 +++ packages/vue/__tests__/e2e/Transition.spec.ts | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index f897f40375f..8d16ba70264 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -264,6 +264,10 @@ const KeepAliveImpl: ComponentOptions = { }) return () => { + if (!slots.default && current && current.type === pendingCacheKey) { + current = null + } + pendingCacheKey = null if (!slots.default) { diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 123d554e3f5..54869782595 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1491,12 +1491,20 @@ describe('e2e: Transition', () => { test( 'replace child and update include at the same time (out-in mode)', async () => { - const updatedSpy = vi.fn() - await page().exposeFunction('updatedSpy', updatedSpy) + const onUpdatedSpyA = vi.fn() + const onUnmountedSpyB = vi.fn() + const onUnmountedSpyC = vi.fn() + + await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA) + await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB) + await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC) + await page().evaluate(() => { - const { updatedSpy } = window as any - const { createApp, ref, shallowRef, h, onUpdated } = (window as any) - .Vue + const { onUpdatedSpyA, onUnmountedSpyB, onUnmountedSpyC } = + window as any + const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = ( + window as any + ).Vue createApp({ template: `
@@ -1514,19 +1522,21 @@ describe('e2e: Transition', () => { CompA: { name: 'CompA', setup() { - onUpdated(updatedSpy) + onUpdated(onUpdatedSpyA) return () => h('div', 'CompA') }, }, CompB: { name: 'CompB', setup() { + onUnmounted(onUnmountedSpyB) return () => h('div', 'CompB') }, }, CompC: { name: 'CompC', setup() { + onUnmounted(onUnmountedSpyC) return () => h('div', 'CompC') }, }, @@ -1557,8 +1567,11 @@ describe('e2e: Transition', () => { await click('#switchToA') await transitionFinish() expect(await html('#container')).toBe('
CompA
') - // expect updatedSpy to be called once - expect(updatedSpy).toBeCalledTimes(1) + + // expect CompA only update once + expect(onUpdatedSpyA).toBeCalledTimes(1) + expect(onUnmountedSpyB).toBeCalledTimes(1) + expect(onUnmountedSpyC).toBeCalledTimes(1) }, E2E_TIMEOUT, ) From f135eee9827a33ee7471aeec9d89e8eb38c0dda1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 20:48:53 +0800 Subject: [PATCH 5/8] chore: update --- packages/vue/__tests__/e2e/Transition.spec.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 54869782595..631c6ab7e57 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1492,16 +1492,13 @@ describe('e2e: Transition', () => { 'replace child and update include at the same time (out-in mode)', async () => { const onUpdatedSpyA = vi.fn() - const onUnmountedSpyB = vi.fn() const onUnmountedSpyC = vi.fn() await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA) - await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB) await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC) await page().evaluate(() => { - const { onUpdatedSpyA, onUnmountedSpyB, onUnmountedSpyC } = - window as any + const { onUpdatedSpyA, onUnmountedSpyC } = window as any const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = ( window as any ).Vue @@ -1529,7 +1526,6 @@ describe('e2e: Transition', () => { CompB: { name: 'CompB', setup() { - onUnmounted(onUnmountedSpyB) return () => h('div', 'CompB') }, }, @@ -1570,7 +1566,6 @@ describe('e2e: Transition', () => { // expect CompA only update once expect(onUpdatedSpyA).toBeCalledTimes(1) - expect(onUnmountedSpyB).toBeCalledTimes(1) expect(onUnmountedSpyC).toBeCalledTimes(1) }, E2E_TIMEOUT, From fe73487b232f7cbaac3b0d957d905d306c49234d Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 2 Sep 2024 21:56:49 +0800 Subject: [PATCH 6/8] wip: save --- packages/runtime-core/src/components/KeepAlive.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 8d16ba70264..07ac2ed2d78 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -264,13 +264,10 @@ const KeepAliveImpl: ComponentOptions = { }) return () => { - if (!slots.default && current && current.type === pendingCacheKey) { - current = null - } - pendingCacheKey = null if (!slots.default) { + current = null return null } From 9c6cd9e4302e03ff762040386ea9bd31cfa263d4 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 3 Sep 2024 22:31:54 +0800 Subject: [PATCH 7/8] wip: save --- packages/runtime-core/src/components/BaseTransition.ts | 1 + packages/runtime-core/src/components/KeepAlive.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index b653e2d1d89..1bb40601efa 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -513,6 +513,7 @@ function getInnerChild(vnode: VNode): VNode | undefined { export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void { if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) { + vnode.transition = hooks setTransitionHooks(vnode.component.subTree, hooks) } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { vnode.ssContent!.transition = hooks.clone(vnode.ssContent!) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 07ac2ed2d78..a5e317368f7 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -267,8 +267,7 @@ const KeepAliveImpl: ComponentOptions = { pendingCacheKey = null if (!slots.default) { - current = null - return null + return (current = null) } const children = slots.default() From 1b897a45c251550d785add5a25013db889b646e6 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 4 Sep 2024 14:53:31 +0800 Subject: [PATCH 8/8] chore: fix #10827 --- .../runtime-core/src/componentRenderUtils.ts | 5 +- .../src/components/BaseTransition.ts | 1 - packages/vue/__tests__/e2e/Transition.spec.ts | 87 ++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 5badb04b006..9fe381ff645 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -60,6 +60,7 @@ export function renderComponentRoot( setupState, ctx, inheritAttrs, + isMounted, } = instance const prev = setCurrentRenderingInstance(instance) @@ -253,7 +254,9 @@ export function renderComponentRoot( `that cannot be animated.`, ) } - root.transition = vnode.transition + root.transition = isMounted + ? vnode.component!.subTree.transition! + : vnode.transition } if (__DEV__ && setRoot) { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 1bb40601efa..b653e2d1d89 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -513,7 +513,6 @@ function getInnerChild(vnode: VNode): VNode | undefined { export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void { if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) { - vnode.transition = hooks setTransitionHooks(vnode.component.subTree, hooks) } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { vnode.ssContent!.transition = hooks.clone(vnode.ssContent!) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 631c6ab7e57..8cdda4dc63e 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1431,7 +1431,7 @@ describe('e2e: Transition', () => { describe('transition with KeepAlive', () => { test( - 'unmount innerChild', + 'unmount innerChild (out-in mode)', async () => { const unmountSpy = vi.fn() await page().exposeFunction('unmountSpy', unmountSpy) @@ -1489,7 +1489,7 @@ describe('e2e: Transition', () => { // #11775 test( - 'replace child and update include at the same time (out-in mode)', + 'switch child then update include (out-in mode)', async () => { const onUpdatedSpyA = vi.fn() const onUnmountedSpyC = vi.fn() @@ -1570,6 +1570,89 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #10827 + test( + 'switch and update child then update include (out-in mode)', + async () => { + const onUnmountedSpyB = vi.fn() + await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB) + + await page().evaluate(() => { + const { onUnmountedSpyB } = window as any + const { + createApp, + ref, + shallowRef, + h, + provide, + inject, + onUnmounted, + } = (window as any).Vue + createApp({ + template: ` +
+ + + + + +
+ + + `, + components: { + CompA: { + name: 'CompA', + setup() { + const current = inject('current') + return () => h('div', current.value) + }, + }, + CompB: { + name: 'CompB', + setup() { + const current = inject('current') + onUnmounted(onUnmountedSpyB) + return () => h('div', current.value) + }, + }, + }, + setup: () => { + const includeRef = ref(['CompA']) + const current = shallowRef('CompA') + provide('current', current) + + const switchToB = () => { + current.value = 'CompB' + includeRef.value = ['CompA', 'CompB'] + } + const switchToA = () => { + current.value = 'CompA' + includeRef.value = ['CompA'] + } + return { current, switchToB, switchToA, includeRef } + }, + }).mount('#app') + }) + + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + await click('#switchToB') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompB
') + + await click('#switchToA') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + expect(onUnmountedSpyB).toBeCalledTimes(1) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => {