Skip to content

Commit c75d471

Browse files
committed
fix(runtime-vapor): properly unmount interop VDOM components
1 parent cb877ff commit c75d471

3 files changed

Lines changed: 161 additions & 7 deletions

File tree

packages/runtime-vapor/__tests__/vdomInterop.spec.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,40 @@ describe('vdomInterop', () => {
15051505
expect(vdomRef.value.name).toBe('vdomChild')
15061506
})
15071507

1508+
it('dynamic component includes vdom component should unmount with vapor branch', async () => {
1509+
const show = ref(true)
1510+
const unmounted = vi.fn()
1511+
const VdomChild = defineComponent({
1512+
setup() {
1513+
onUnmounted(unmounted)
1514+
return () => h('div', 'vdom child')
1515+
},
1516+
})
1517+
1518+
const VaporChild = defineVaporComponent({
1519+
setup() {
1520+
return createIf(
1521+
() => show.value,
1522+
() => createDynamicComponent(() => VdomChild),
1523+
)
1524+
},
1525+
})
1526+
1527+
const { html } = define({
1528+
setup() {
1529+
return () => h(VaporChild as any)
1530+
},
1531+
}).render()
1532+
1533+
expect(html()).toContain('<div>vdom child</div>')
1534+
1535+
show.value = false
1536+
await nextTick()
1537+
1538+
expect(unmounted).toHaveBeenCalledTimes(1)
1539+
expect(html()).not.toContain('vdom child')
1540+
})
1541+
15081542
it('dynamic component includes vdom component should cleanup old ref', async () => {
15091543
const VdomChild = defineComponent({
15101544
setup(_, { expose }) {
@@ -1906,6 +1940,121 @@ describe('vdomInterop', () => {
19061940
expect(hooksA.activated).toHaveBeenCalledTimes(2)
19071941
})
19081942

1943+
it('unmounts cached inner VDOM components', async () => {
1944+
const hooksA = {
1945+
unmounted: vi.fn(),
1946+
}
1947+
const hooksB = {
1948+
unmounted: vi.fn(),
1949+
}
1950+
1951+
const VDOMCompA = defineComponent({
1952+
setup() {
1953+
onUnmounted(() => hooksA.unmounted())
1954+
return () => h('div', 'vdom A')
1955+
},
1956+
})
1957+
1958+
const VDOMCompB = defineComponent({
1959+
setup() {
1960+
onUnmounted(() => hooksB.unmounted())
1961+
return () => h('div', 'vdom B')
1962+
},
1963+
})
1964+
1965+
const current = shallowRef<any>(VDOMCompA)
1966+
1967+
const App = defineVaporComponent({
1968+
setup() {
1969+
return createComponent(VaporKeepAlive, null, {
1970+
default: () => createDynamicComponent(() => current.value),
1971+
})
1972+
},
1973+
})
1974+
1975+
const root = document.createElement('div')
1976+
const app = createApp({
1977+
setup() {
1978+
return () => h(App as any)
1979+
},
1980+
})
1981+
app.use(vaporInteropPlugin)
1982+
app.mount(root)
1983+
1984+
expect(root.innerHTML).toBe(
1985+
'<div>vdom A</div><!--dynamic-component-->',
1986+
)
1987+
1988+
current.value = VDOMCompB
1989+
await nextTick()
1990+
expect(root.innerHTML).toBe(
1991+
'<div>vdom B</div><!--dynamic-component-->',
1992+
)
1993+
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
1994+
expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
1995+
1996+
app.unmount()
1997+
await nextTick()
1998+
expect(hooksA.unmounted).toHaveBeenCalledTimes(1)
1999+
expect(hooksB.unmounted).toHaveBeenCalledTimes(1)
2000+
})
2001+
2002+
it('unmounts inactive cached inner VDOM components during KeepAlive hmr rerender', async () => {
2003+
const hooksA = {
2004+
unmounted: vi.fn(),
2005+
}
2006+
const hooksB = {
2007+
unmounted: vi.fn(),
2008+
}
2009+
2010+
const VDOMCompA = defineComponent({
2011+
setup() {
2012+
onUnmounted(() => hooksA.unmounted())
2013+
return () => h('div', 'vdom A')
2014+
},
2015+
})
2016+
2017+
const VDOMCompB = defineComponent({
2018+
setup() {
2019+
onUnmounted(() => hooksB.unmounted())
2020+
return () => h('div', 'vdom B')
2021+
},
2022+
})
2023+
2024+
const current = shallowRef<any>(VDOMCompA)
2025+
let keepAlive: any
2026+
2027+
const App = defineVaporComponent({
2028+
setup() {
2029+
keepAlive = createComponent(VaporKeepAlive, null, {
2030+
default: () => createDynamicComponent(() => current.value),
2031+
})
2032+
return keepAlive
2033+
},
2034+
})
2035+
2036+
const { html } = define({
2037+
setup() {
2038+
return () => h(App as any)
2039+
},
2040+
}).render()
2041+
2042+
expect(html()).toBe('<div>vdom A</div><!--dynamic-component-->')
2043+
2044+
current.value = VDOMCompB
2045+
await nextTick()
2046+
expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
2047+
expect(hooksA.unmounted).toHaveBeenCalledTimes(0)
2048+
expect(hooksB.unmounted).toHaveBeenCalledTimes(0)
2049+
2050+
keepAlive.hmrRerender()
2051+
await nextTick()
2052+
2053+
expect(hooksA.unmounted).toHaveBeenCalledTimes(1)
2054+
expect(hooksB.unmounted).toHaveBeenCalledTimes(1)
2055+
expect(html()).toBe('<div>vdom B</div><!--dynamic-component-->')
2056+
})
2057+
19092058
it('switch VNode with inner mixed vapor/VDOM components', async () => {
19102059
const hooksA = {
19112060
mounted: vi.fn(),

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,15 @@ const VaporKeepAliveImpl = defineVaporComponent({
125125
const rerender = keepAliveInstance.hmrRerender
126126
keepAliveInstance.hmrRerender = () => {
127127
keepAliveInstance.exposed = null
128-
cache.forEach(cached => unsetShapeFlag(cached))
128+
cache.forEach(cached => {
129+
unsetShapeFlag(cached)
130+
if (cached !== current) {
131+
// Cached blocks may contain interop children whose VDOM teardown
132+
// is owned by remove(), not scope.stop().
133+
const parentNode = findBlockNode(cached).parentNode
134+
if (parentNode) remove(cached, parentNode as ParentNode)
135+
}
136+
})
129137
cache.clear()
130138
keys.clear()
131139
keptAliveScopes.forEach(scope => scope.stop())

packages/runtime-vapor/src/vdomInterop.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ function createVDOMComponent(
919919
}
920920
const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
921921
if (isUnmounted) {
922-
removeDom(parentNode)
922+
if (!transition) removeDom(parentNode)
923923
return
924924
}
925925
// unset ref
@@ -935,18 +935,16 @@ function createVDOMComponent(
935935
)
936936
return
937937
}
938-
// Vapor block removal and scope disposal can both reach this path.
939-
// VDOM fragment ranges must only be removed once.
940938
isUnmounted = true
941939
isMounted = false
942940
internals.umt(vnode.component!, null, !!parentNode)
943-
removeDom(parentNode)
941+
// VDOM transitions own their leaving DOM until the leave finishes.
942+
if (!transition) removeDom(parentNode)
944943
}
945944

946945
frag.hydrate = () => {
947946
if (!isHydrating) return
948947
hydrateVNode(vnode, parentComponent as any)
949-
onScopeDispose(unmount, true)
950948
isMounted = true
951949
frag.nodes = resolveVNodeNodes(vnode)
952950
frag.validityPending = false
@@ -984,7 +982,6 @@ function createVDOMComponent(
984982
)
985983
// set ref
986984
if (rawRef) vdomSetRef(rawRef, null, null, vnode)
987-
onScopeDispose(unmount, true)
988985
isMounted = true
989986
} else {
990987
// move

0 commit comments

Comments
 (0)