]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): ensure correct anchor el for unresolved async components (#13560)
authorlinzhe <40790268+linzhe141@users.noreply.github.com>
Wed, 23 Jul 2025 00:42:10 +0000 (08:42 +0800)
committerGitHub <noreply@github.com>
Wed, 23 Jul 2025 00:42:10 +0000 (08:42 +0800)
close #13559

packages/runtime-core/__tests__/components/Suspense.spec.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts

index 65e801de277f767d4784ced162b1c3f1cfd6e0ef..563c91a179df2cc6f36b811b9e7b6e1ae9dd6cfa 100644 (file)
@@ -2230,5 +2230,57 @@ describe('Suspense', () => {
         fallback: [h('div'), h('div')],
       })
     })
+
+    // #13559
+    test('renders multiple async components in Suspense with v-for and updates on items change', async () => {
+      const CompAsyncSetup = defineAsyncComponent({
+        props: ['item'],
+        render(ctx: any) {
+          return h('div', ctx.item.name)
+        },
+      })
+
+      const items = ref([
+        { id: 1, name: '111' },
+        { id: 2, name: '222' },
+        { id: 3, name: '333' },
+      ])
+
+      const Comp = {
+        setup() {
+          return () =>
+            h(Suspense, null, {
+              default: () =>
+                h(
+                  Fragment,
+                  null,
+                  items.value.map(item =>
+                    h(CompAsyncSetup, { item, key: item.id }),
+                  ),
+                ),
+            })
+        },
+      }
+
+      const root = nodeOps.createElement('div')
+      render(h(Comp), root)
+      await nextTick()
+      await Promise.all(deps)
+
+      expect(serializeInner(root)).toBe(
+        `<div>111</div><div>222</div><div>333</div>`,
+      )
+
+      items.value = [
+        { id: 4, name: '444' },
+        { id: 5, name: '555' },
+        { id: 6, name: '666' },
+      ]
+      await nextTick()
+      await Promise.all(deps)
+      expect(serializeInner(root)).toBe(
+        `<div>444</div><div>555</div><div>666</div>`,
+      )
+    })
   })
 })
index a57be791a44eb807796520ea2833bc2ea7ee6cbe..f046e93ad85af04d0c4191e84e6fb7751e65bd69 100644 (file)
@@ -1226,6 +1226,7 @@ function baseCreateRenderer(
       if (!initialVNode.el) {
         const placeholder = (instance.subTree = createVNode(Comment))
         processCommentNode(null, placeholder, container!, anchor)
+        initialVNode.placeholder = placeholder.el
       }
     } else {
       setupRenderEffect(
@@ -1979,8 +1980,12 @@ function baseCreateRenderer(
       for (i = toBePatched - 1; i >= 0; i--) {
         const nextIndex = s2 + i
         const nextChild = c2[nextIndex] as VNode
+        const anchorVNode = c2[nextIndex + 1] as VNode
         const anchor =
-          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
+          nextIndex + 1 < l2
+            ? // #13559, fallback to el placeholder for unresolved async component
+              anchorVNode.el || anchorVNode.placeholder
+            : parentAnchor
         if (newIndexToOldIndexMap[i] === 0) {
           // mount new
           patch(
index a8c5340cd1fe167840f700e34a2a25b1909eb048..cd1ef948d73aa8bacf0bfb44c93ebf7732ee7e49 100644 (file)
@@ -196,6 +196,7 @@ export interface VNode<
 
   // DOM
   el: HostNode | null
+  placeholder: HostNode | null // async component el placeholder
   anchor: HostNode | null // fragment anchor
   target: HostElement | null // teleport target
   targetStart: HostNode | null // teleport target start anchor
@@ -711,6 +712,8 @@ export function cloneVNode<T, U>(
     suspense: vnode.suspense,
     ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
     ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
+    placeholder: vnode.placeholder,
+
     el: vnode.el,
     anchor: vnode.anchor,
     ctx: vnode.ctx,