From 772b0087cb7be151c514a1d30365fb0f61a652ba Mon Sep 17 00:00:00 2001 From: edison Date: Fri, 16 May 2025 08:32:55 +0800 Subject: [PATCH] fix(suspense): handle edge case in patching list nodes within Suspense (#13306) close #13305 --- .../__tests__/rendererOptimizedMode.spec.ts | 108 ++++++++++++++++++ packages/runtime-core/src/renderer.ts | 3 +- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index 958c12748..1d8bb8420 100644 --- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts +++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts @@ -861,6 +861,114 @@ describe('renderer: optimized mode', () => { expect(inner(root)).toBe('
true
') }) + // #13305 + test('patch Suspense nested in list nodes in optimized mode', async () => { + const deps: Promise[] = [] + + const Item = { + props: { + someId: { type: Number, required: true }, + }, + async setup(props: any) { + const p = new Promise(resolve => setTimeout(resolve, 1)) + deps.push(p) + + await p + return () => ( + openBlock(), + createElementBlock('li', null, [ + createElementVNode( + 'p', + null, + String(props.someId), + PatchFlags.TEXT, + ), + ]) + ) + }, + } + + const list = ref([1, 2, 3]) + const App = { + setup() { + return () => ( + openBlock(), + createElementBlock( + Fragment, + null, + [ + createElementVNode( + 'p', + null, + JSON.stringify(list.value), + PatchFlags.TEXT, + ), + createElementVNode('ol', null, [ + (openBlock(), + createBlock(SuspenseImpl, null, { + fallback: withCtx(() => [ + createElementVNode('li', null, 'Loading…'), + ]), + default: withCtx(() => [ + (openBlock(true), + createElementBlock( + Fragment, + null, + renderList(list.value, id => { + return ( + openBlock(), + createBlock( + Item, + { + key: id, + 'some-id': id, + }, + null, + PatchFlags.PROPS, + ['some-id'], + ) + ) + }), + PatchFlags.KEYED_FRAGMENT, + )), + ]), + _: 1 /* STABLE */, + })), + ]), + ], + PatchFlags.STABLE_FRAGMENT, + ) + ) + }, + } + + const app = createApp(App) + app.mount(root) + expect(inner(root)).toBe(`

[1,2,3]

` + `
  1. Loading…
`) + + await Promise.all(deps) + await nextTick() + expect(inner(root)).toBe( + `

[1,2,3]

` + + `
    ` + + `
  1. 1

  2. ` + + `
  3. 2

  4. ` + + `
  5. 3

  6. ` + + `
`, + ) + + list.value = [3, 1, 2] + await nextTick() + expect(inner(root)).toBe( + `

[3,1,2]

` + + `
    ` + + `
  1. 3

  2. ` + + `
  3. 1

  4. ` + + `
  5. 2

  6. ` + + `
`, + ) + }) + // #4183 test('should not take unmount children fast path /w Suspense', async () => { const show = ref(true) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 334858b7b..7b39aa917 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -961,7 +961,8 @@ function baseCreateRenderer( // which also requires the correct parent container !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. - oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT)) + oldVNode.shapeFlag & + (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT | ShapeFlags.SUSPENSE)) ? hostParentNode(oldVNode.el)! : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call.