diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index 58076fff9ee..ff7aa56d055 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -10,6 +10,7 @@ import {
insert,
prepend,
renderEffect,
+ setInsertionState,
template,
} from '../src'
import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
@@ -502,5 +503,35 @@ describe('component: slots', () => {
await nextTick()
expect(host.innerHTML).toBe('
')
})
+
+ test('consecutive slots with insertion state', async () => {
+ const { component: Child } = define({
+ setup() {
+ const n2 = template('', true)() as any
+ setInsertionState(n2, 0)
+ createSlot('default', null)
+ setInsertionState(n2, 0)
+ createSlot('foo', null)
+ return n2
+ },
+ })
+
+ const { html } = define({
+ setup() {
+ return createComponent(Child, null, {
+ default: () => template('default')(),
+ foo: () => template('foo')(),
+ })
+ },
+ }).render()
+
+ expect(html()).toBe(
+ `` +
+ `default` +
+ `foo` +
+ `
baz
` +
+ `
`,
+ )
+ })
})
})
diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts
index b782afd38d3..943bda67ca5 100644
--- a/packages/runtime-vapor/src/block.ts
+++ b/packages/runtime-vapor/src/block.ts
@@ -105,10 +105,10 @@ export function isValidBlock(block: Block): boolean {
export function insert(
block: Block,
- parent: ParentNode,
+ parent: ParentNode & { $anchor?: Node | null },
anchor: Node | null | 0 = null, // 0 means prepend
): void {
- anchor = anchor === 0 ? parent.firstChild : anchor
+ anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor
if (block instanceof Node) {
if (!isHydrating) {
parent.insertBefore(block, anchor)
diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts
index 8c42ad766a5..3b663da7715 100644
--- a/packages/runtime-vapor/src/dom/prop.ts
+++ b/packages/runtime-vapor/src/dom/prop.ts
@@ -269,7 +269,7 @@ export function optimizePropertyLookup(): void {
if (isOptimized) return
isOptimized = true
const proto = Element.prototype as any
- proto.$evtclick = undefined
+ proto.$anchor = proto.$evtclick = undefined
proto.$root = false
proto.$html =
proto.$txt =
diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts
index c8c7ffbcd1d..8c66843bd93 100644
--- a/packages/runtime-vapor/src/insertionState.ts
+++ b/packages/runtime-vapor/src/insertionState.ts
@@ -6,7 +6,18 @@ export let insertionAnchor: Node | 0 | undefined
* (component, slot outlet, if, for) is created. The state is used for actual
* insertion on client-side render, and used for node adoption during hydration.
*/
-export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
+export function setInsertionState(
+ parent: ParentNode & { $anchor?: Node | null },
+ anchor?: Node | 0,
+): void {
+ // When setInsertionState(n3, 0) is called consecutively, the first prepend operation
+ // uses parent.firstChild as the anchor. However, after insertion, parent.firstChild
+ // changes and cannot serve as the anchor for subsequent prepends. Therefore, we cache
+ // the original parent.firstChild on the first call for subsequent prepend operations.
+ if (anchor === 0 && !parent.$anchor) {
+ parent.$anchor = parent.firstChild
+ }
+
insertionParent = parent
insertionAnchor = anchor
}