fix(runtime-core): ensure correct anchor el for deeper unresolved async components#14182
Conversation
WalkthroughReplaces a direct fallback to an anchor vnode placeholder with an internal resolver that recursively inspects component wrapper VNodes to locate async component placeholders; adds a test exercising Suspense + v-for with nested async wrappers and list updates. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)packages/runtime-core/__tests__/components/Suspense.spec.ts (3)
🔇 Additional comments (1)
Comment |
Size ReportBundles
Usages
|
@vue/compiler-core
@vue/compiler-dom
@vue/compiler-sfc
@vue/compiler-ssr
@vue/reactivity
@vue/runtime-core
@vue/runtime-dom
@vue/server-renderer
@vue/shared
vue
@vue/compat
commit: |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
packages/runtime-core/src/renderer.ts (1)
2581-2602: Consider adding defensive safeguards for edge cases.The recursive traversal logic correctly handles the wrapper component scenario described in issue #14173. However, consider adding safeguards:
Null check for subTree: Line 2590 accesses
asyncWrapper.subTreewithout verification. If a component exists butsubTreeis undefined (e.g., during initialization or edge cases), this could cause issues.Cycle detection or depth limit: While component trees should be acyclic, a depth limit or cycle detection would prevent potential infinite recursion if the component structure is unexpected.
Apply this diff to add defensive checks:
function resolveAsyncComponentPlaceholder(anchorVnode: VNode) { // anchor vnode is a unresolved async component if (anchorVnode.placeholder) { return anchorVnode.placeholder } // anchor vnode maybe is a wrapper component has single unresolved async component const asyncWrapper = anchorVnode.component if (asyncWrapper) { const subTree = asyncWrapper.subTree + + // guard against missing subTree + if (!subTree) { + return null + } // wrapper that directly contains an unresolved async component if (subTree.placeholder) { return subTree.placeholder } // try to locate deeper nested async component placeholder return resolveAsyncComponentPlaceholder(subTree) } return null }Alternatively, consider adding a depth parameter to prevent excessive recursion:
function resolveAsyncComponentPlaceholder(anchorVnode: VNode, depth: number = 0): HostNode | null { const MAX_DEPTH = 10 // reasonable limit for nested wrappers if (depth > MAX_DEPTH) { if (__DEV__) { warn('Maximum depth exceeded while resolving async component placeholder') } return null } if (anchorVnode.placeholder) { return anchorVnode.placeholder } const asyncWrapper = anchorVnode.component if (asyncWrapper?.subTree) { if (asyncWrapper.subTree.placeholder) { return asyncWrapper.subTree.placeholder } return resolveAsyncComponentPlaceholder(asyncWrapper.subTree, depth + 1) } return null }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/runtime-core/src/renderer.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/runtime-core/src/renderer.ts (1)
packages/runtime-core/src/vnode.ts (1)
VNode(160-257)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: test / e2e-test
🔇 Additional comments (1)
packages/runtime-core/src/renderer.ts (1)
1999-1999: LGTM! Correctly handles nested async component placeholders.The fallback chain (
anchorVNode.el || resolveAsyncComponentPlaceholder(anchorVNode)) appropriately addresses the edge case where wrapper components contain unresolved async components. This ensures the correct anchor is found even when there are intermediate wrapper elements.
|
/ecosystem-ci run |
|
📝 Ran ecosystem CI: Open
|
| }) | ||
|
|
||
| // #14173 | ||
| test('nested async components with v-for + only Suspense and async component wrappers', async () => { |
There was a problem hiding this comment.
If you want an even smaller reproduction:
const items = ref([4, 5, 6]);
const App = {
setup() {
const loader = async () => defineComponent({
props: {item: Number},
setup(props) {
return () => h('div', props.item)
}
});
const LazyTest = defineAsyncComponent({
loader: async () => defineComponent({
setup(_, ctx) {
const comp = defineAsyncComponent({
loader: loader,
})
return () => h(comp, ctx.attrs)
}
}),
})
return () =>
h(Suspense, null, {
default: () =>
h(
Fragment,
null,
items.value.map(item =>
h(LazyTest, {id: item, key: item}),
),
),
})
},
}
const root = nodeOps.createElement('div')
render(h(App), root)
await nextTick()
await Promise.all(deps)
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>4</div><div>5</div><div>6</div>`,
)
items.value = [1,2,3]
await nextTick()
await Promise.all(deps)
await Promise.all(deps)
expect(serializeInner(root)).toBe(
`<div>1</div><div>2</div><div>3</div>`,
)There was a problem hiding this comment.
Thank you for your review. This unit test case was designed to reproduce nested async component wrappers and nested suspense wrappers, so it was a bit complex. I've now simplified the HTML to make it look less complicated.
close #14173
This is a follow-up to this PR #13560.
The following issue examples are just a sufficiently complex case to describe the current issues with
patchKeyedChildren + async component wrapper, this maby a edge case.this pr playground
issue playground
Summary by CodeRabbit
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.