Skip to content

Commit d6a6ec1

Browse files
authored
fix(runtime-core): prevent unmounted vnode from being inserted during transition leave (#12862)
close #12860
1 parent 263f63f commit d6a6ec1

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

packages/runtime-core/src/components/KeepAlive.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ const KeepAliveImpl: ComponentOptions = {
187187
// Update components tree
188188
devtoolsComponentAdded(instance)
189189
}
190+
191+
// for e2e test
192+
if (__DEV__ && __BROWSER__) {
193+
;(instance as any).__keepAliveStorageContainer = storageContainer
194+
}
190195
}
191196

192197
function unmount(vnode: VNode) {

packages/runtime-core/src/renderer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2049,7 +2049,13 @@ function baseCreateRenderer(
20492049
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
20502050
} else {
20512051
const { leave, delayLeave, afterLeave } = transition!
2052-
const remove = () => hostInsert(el!, container, anchor)
2052+
const remove = () => {
2053+
if (vnode.ctx!.isUnmounted) {
2054+
hostRemove(el!)
2055+
} else {
2056+
hostInsert(el!, container, anchor)
2057+
}
2058+
}
20532059
const performLeave = () => {
20542060
leave(el!, () => {
20552061
remove()

packages/vue/__tests__/e2e/Transition.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ElementHandle } from 'puppeteer'
12
import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
23
import path from 'node:path'
34
import { Transition, createApp, h, nextTick, ref } from 'vue'
@@ -1653,6 +1654,74 @@ describe('e2e: Transition', () => {
16531654
},
16541655
E2E_TIMEOUT,
16551656
)
1657+
1658+
// #12860
1659+
test(
1660+
'unmount children',
1661+
async () => {
1662+
const unmountSpy = vi.fn()
1663+
let storageContainer: ElementHandle<HTMLDivElement>
1664+
const setStorageContainer = (container: any) =>
1665+
(storageContainer = container)
1666+
await page().exposeFunction('unmountSpy', unmountSpy)
1667+
await page().exposeFunction('setStorageContainer', setStorageContainer)
1668+
await page().evaluate(() => {
1669+
const { unmountSpy, setStorageContainer } = window as any
1670+
const { createApp, ref, h, onUnmounted, getCurrentInstance } = (
1671+
window as any
1672+
).Vue
1673+
createApp({
1674+
template: `
1675+
<div id="container">
1676+
<transition>
1677+
<KeepAlive :include="includeRef">
1678+
<TrueBranch v-if="toggle"></TrueBranch>
1679+
</KeepAlive>
1680+
</transition>
1681+
</div>
1682+
<button id="toggleBtn" @click="click">button</button>
1683+
`,
1684+
components: {
1685+
TrueBranch: {
1686+
name: 'TrueBranch',
1687+
setup() {
1688+
const instance = getCurrentInstance()
1689+
onUnmounted(() => {
1690+
unmountSpy()
1691+
setStorageContainer(instance.__keepAliveStorageContainer)
1692+
})
1693+
const count = ref(0)
1694+
return () => h('div', count.value)
1695+
},
1696+
},
1697+
},
1698+
setup: () => {
1699+
const includeRef = ref(['TrueBranch'])
1700+
const toggle = ref(true)
1701+
const click = () => {
1702+
toggle.value = !toggle.value
1703+
if (toggle.value) {
1704+
includeRef.value = ['TrueBranch']
1705+
} else {
1706+
includeRef.value = []
1707+
}
1708+
}
1709+
return { toggle, click, unmountSpy, includeRef }
1710+
},
1711+
}).mount('#app')
1712+
})
1713+
1714+
await transitionFinish()
1715+
expect(await html('#container')).toBe('<div>0</div>')
1716+
1717+
await click('#toggleBtn')
1718+
await transitionFinish()
1719+
expect(await html('#container')).toBe('<!--v-if-->')
1720+
expect(unmountSpy).toBeCalledTimes(1)
1721+
expect(await storageContainer!.evaluate(x => x.innerHTML)).toBe(``)
1722+
},
1723+
E2E_TIMEOUT,
1724+
)
16561725
})
16571726

16581727
describe('transition with Suspense', () => {

0 commit comments

Comments
 (0)