From e0253c1c7f04f4f9478fd6f307d1275cce7aa8cd Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 20 Feb 2025 23:04:19 +0800 Subject: [PATCH 1/3] fix(runtime-core): synchronously update Suspense vnode's el during component self-update --- packages/runtime-core/src/componentRenderUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index a1afae6201a..5e5e159aae0 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -451,7 +451,7 @@ function hasPropsChanged( } export function updateHOCHostEl( - { vnode, parent }: ComponentInternalInstance, + { vnode, parent, suspense }: ComponentInternalInstance, el: typeof vnode.el, // HostNode ): void { while (parent) { @@ -461,6 +461,7 @@ export function updateHOCHostEl( } if (root === vnode) { ;(vnode = parent.vnode).el = el + if (suspense && suspense.activeBranch === vnode) suspense.vnode.el = el parent = parent.parent } else { break From c52332a03f13ac7a22f54201b94a327c1e323b4e Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Feb 2025 09:07:25 +0800 Subject: [PATCH 2/3] test: add tests --- .../__tests__/components/Suspense.spec.ts | 61 +++++++++++++++++++ .../runtime-core/src/componentRenderUtils.ts | 7 ++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 65e801de277..0944c40d6b2 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -8,13 +8,16 @@ import { KeepAlive, Suspense, type SuspenseProps, + createBlock, createCommentVNode, + createElementBlock, h, nextTick, nodeOps, onErrorCaptured, onMounted, onUnmounted, + openBlock, ref, render, resolveDynamicComponent, @@ -26,6 +29,7 @@ import { import { computed, createApp, defineComponent, inject, provide } from 'vue' import type { RawSlots } from 'packages/runtime-core/src/componentSlots' import { resetSuspenseId } from '../../src/components/Suspense' +import { PatchFlags } from '@vue/shared' describe('Suspense', () => { const deps: Promise[] = [] @@ -2161,6 +2165,63 @@ describe('Suspense', () => { await Promise.all(deps) }) + // #12920 + test('unmount Suspense after children self-update', async () => { + const Comp = defineAsyncComponent({ + setup() { + const show = ref(true) + onMounted(() => { + // trigger self-update + show.value = !show.value + }) + return () => + show.value + ? (openBlock(), createElementBlock('div', { key: 0 }, 'show')) + : (openBlock(), createElementBlock('div', { key: 1 }, 'hidden')) + }, + }) + + const toggle = ref(true) + const root = nodeOps.createElement('div') + const App = { + render() { + return ( + openBlock(), + createElementBlock( + Fragment, + null, + [ + h('h1', null, toggle.value), + toggle.value + ? (openBlock(), + createBlock( + Suspense, + { key: 0 }, + { + default: h(Comp), + }, + )) + : createCommentVNode('v-if', true), + ], + PatchFlags.STABLE_FRAGMENT, + ) + ) + }, + } + render(h(App), root) + expect(serializeInner(root)).toBe(`

true

`) + + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`

true

hidden
`) + + // unmount suspense + toggle.value = false + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`

true

`) + }) + describe('warnings', () => { // base function to check if a combination of slots warns or not function baseCheckWarn( diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 5e5e159aae0..c9cc5ded7c2 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -457,14 +457,17 @@ export function updateHOCHostEl( while (parent) { const root = parent.subTree if (root.suspense && root.suspense.activeBranch === vnode) { - root.el = vnode.el + root.suspense.vnode.el = root.el = vnode.el } if (root === vnode) { ;(vnode = parent.vnode).el = el - if (suspense && suspense.activeBranch === vnode) suspense.vnode.el = el parent = parent.parent } else { break } } + // also update suspense vnode el + if (suspense && suspense.activeBranch === vnode) { + suspense.vnode.el = el + } } From 2c12b58058f002289085b04695143ef6bdb26038 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 21 Feb 2025 21:59:44 +0800 Subject: [PATCH 3/3] test: add more cases --- .../__tests__/components/Suspense.spec.ts | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 0944c40d6b2..999e18f4b93 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -26,7 +26,14 @@ import { watch, watchEffect, } from '@vue/runtime-test' -import { computed, createApp, defineComponent, inject, provide } from 'vue' +import { + computed, + createApp, + defineAsyncComponent as defineAsyncComp, + defineComponent, + inject, + provide, +} from 'vue' import type { RawSlots } from 'packages/runtime-core/src/componentSlots' import { resetSuspenseId } from '../../src/components/Suspense' import { PatchFlags } from '@vue/shared' @@ -2166,12 +2173,12 @@ describe('Suspense', () => { }) // #12920 - test('unmount Suspense after children self-update', async () => { - const Comp = defineAsyncComponent({ + test('unmount Suspense after async child (with defineAsyncComponent) self-triggered update', async () => { + const Comp = defineComponent({ setup() { const show = ref(true) onMounted(() => { - // trigger self-update + // trigger update show.value = !show.value }) return () => @@ -2181,6 +2188,77 @@ describe('Suspense', () => { }, }) + const AsyncComp = defineAsyncComp(() => { + const p = new Promise(resolve => { + resolve(Comp) + }) + deps.push(p.then(() => Promise.resolve())) + return p as any + }) + + const toggle = ref(true) + const root = nodeOps.createElement('div') + const App = { + render() { + return ( + openBlock(), + createElementBlock( + Fragment, + null, + [ + h('h1', null, toggle.value), + toggle.value + ? (openBlock(), + createBlock( + Suspense, + { key: 0 }, + { + default: h(AsyncComp), + }, + )) + : createCommentVNode('v-if', true), + ], + PatchFlags.STABLE_FRAGMENT, + ) + ) + }, + } + render(h(App), root) + expect(serializeInner(root)).toBe(`

true

`) + + await Promise.all(deps) + await nextTick() + await nextTick() + expect(serializeInner(root)).toBe(`

true

show
`) + + await nextTick() + expect(serializeInner(root)).toBe(`

true

hidden
`) + + // unmount suspense + toggle.value = false + await Promise.all(deps) + await nextTick() + expect(serializeInner(root)).toBe(`

true

`) + }) + + test('unmount Suspense after async child (with async setup) self-triggered update', async () => { + const AsyncComp = defineComponent({ + async setup() { + const show = ref(true) + onMounted(() => { + // trigger update + show.value = !show.value + }) + const p = new Promise(r => setTimeout(r, 1)) + // extra tick needed for Node 12+ + deps.push(p.then(() => Promise.resolve())) + return () => + show.value + ? (openBlock(), createElementBlock('div', { key: 0 }, 'show')) + : (openBlock(), createElementBlock('div', { key: 1 }, 'hidden')) + }, + }) + const toggle = ref(true) const root = nodeOps.createElement('div') const App = { @@ -2198,7 +2276,7 @@ describe('Suspense', () => { Suspense, { key: 0 }, { - default: h(Comp), + default: h(AsyncComp), }, )) : createCommentVNode('v-if', true),