diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 4e8da3288f1..056758729ba 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -2360,6 +2360,101 @@ describe('Suspense', () => { ) }) + // #14173 + test('nested async components with v-for + only Suspense and async component wrappers', async () => { + const CompAsyncSetup = defineAsyncComponent({ + props: ['item', 'id'], + render(ctx: any) { + return h('div', ctx.id + '-' + ctx.item.name) + }, + }) + const items = ref([ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + ]) + const Comp = { + props: ['id'], + setup(props: any) { + return () => + h(Suspense, null, { + default: () => + h( + Fragment, + null, + items.value.map(item => + h(CompAsyncSetup, { + item, + key: item.id, + id: props.id, + }), + ), + ), + }) + }, + } + + const CompAsyncWrapper = defineAsyncComponent({ + props: ['id'], + render(ctx: any) { + return h(Comp, { id: ctx.id }) + }, + }) + const CompWrapper = defineComponent({ + props: ['id'], + render(ctx: any) { + return h(CompAsyncWrapper, { id: ctx.id }) + }, + }) + const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }]) + + const App = { + setup() { + return () => + h(Suspense, null, { + default: () => + h( + Fragment, + null, + list.value.map(item => + h(CompWrapper, { id: item.id, key: item.id }), + ), + ), + }) + }, + } + + const root = nodeOps.createElement('div') + render(h(App), root) + await nextTick() + await Promise.all(deps) + await Promise.all(deps) + + expect(serializeInner(root)).toBe( + `
1-a
1-b
1-c
2-a
2-b
2-c
3-a
3-b
3-c
`, + ) + + list.value = [{ id: 4 }, { id: 5 }, { id: 6 }] + await nextTick() + await Promise.all(deps) + await Promise.all(deps) + expect(serializeInner(root)).toBe( + `
4-a
4-b
4-c
5-a
5-b
5-c
6-a
6-b
6-c
`, + ) + + items.value = [ + { id: 4, name: 'd' }, + { id: 5, name: 'f' }, + { id: 6, name: 'g' }, + ] + await nextTick() + await Promise.all(deps) + await Promise.all(deps) + expect(serializeInner(root)).toBe( + `
4-d
4-f
4-g
5-d
5-f
5-g
6-d
6-f
6-g
`, + ) + }) + test('should call unmounted directive once when fallback is replaced by resolved async component', async () => { const Comp = { render() { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 192bb44474e..5aa12895f36 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1995,8 +1995,8 @@ function baseCreateRenderer( const anchorVNode = c2[nextIndex + 1] as VNode const anchor = nextIndex + 1 < l2 - ? // #13559, fallback to el placeholder for unresolved async component - anchorVNode.el || anchorVNode.placeholder + ? // #13559, #14173 fallback to el placeholder for unresolved async component + anchorVNode.el || resolveAsyncComponentPlaceholder(anchorVNode) : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // mount new @@ -2577,3 +2577,17 @@ export function invalidateMount(hooks: LifecycleHook): void { hooks[i].flags! |= SchedulerJobFlags.DISPOSED } } + +function resolveAsyncComponentPlaceholder(anchorVnode: VNode) { + if (anchorVnode.placeholder) { + return anchorVnode.placeholder + } + + // anchor vnode maybe is a wrapper component has single unresolved async component + const instance = anchorVnode.component + if (instance) { + return resolveAsyncComponentPlaceholder(instance.subTree) + } + + return null +}