wip: v-for hydration

This commit is contained in:
daiwei 2025-04-25 17:08:07 +08:00
parent aad75fd7c4
commit e6e016016f
6 changed files with 276 additions and 14 deletions

View File

@ -15,7 +15,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--]-->\`)
_push(\`<!--for--><!--]-->\`)
}"
`)
})
@ -33,7 +33,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
_push(\`<!--for--></ul>\`)
}"
`)
})
@ -52,6 +52,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
if (false) {
_push(\`<div></div>\`)
_push(\`<!--if-->\`)
@ -75,7 +76,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</ul>\`)
_push(\`<!--for--></ul>\`)
}"
`)
})
@ -97,7 +98,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`<div></div>\`)
})
_push(\`</\${_ctx.someTag}>\`)
_push(\`<!--for--></\${_ctx.someTag}>\`)
}"
`)
})
@ -119,9 +120,11 @@ describe('transition-group', () => {
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
_ssrRenderList(10, (i) => {
_push(\`<div></div>\`)
})
_push(\`<!--for-->\`)
if (_ctx.ok) {
_push(\`<div>ok</div>\`)
_push(\`<!--if-->\`)

View File

@ -13,6 +13,7 @@ import {
processChildrenAsStatement,
} from '../ssrCodegenTransform'
import { SSR_RENDER_LIST } from '../runtimeHelpers'
import { FOR_ANCHOR_LABEL } from '@vue/shared'
// Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformFor: NodeTransform =
@ -48,5 +49,8 @@ export function ssrProcessFor(
)
if (!disableNestedFragments) {
context.pushStringPart(`<!--]-->`)
} else {
// add anchor for non-fragment v-for
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
}
}

View File

@ -1123,6 +1123,73 @@ describe('Vapor Mode hydration', () => {
})
})
test('on fragment component', async () => {
runWithEnv(isProd, async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<div>
<components.Child v-if="data"/>
</div>
</template>`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><div>true</div>-true-<!--]-->` +
`<!--if-->` +
`</div>`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toBe(
`<div>` + `<!--[--><!--]-->` + `<!--${anchorLabel}-->` + `</div>`,
)
})
})
test('on fragment component with anchor insertion', async () => {
runWithEnv(isProd, async () => {
const data = ref(true)
const { container } = await testHydration(
`<template>
<div>
<span/>
<components.Child v-if="data"/>
<span/>
</div>
</template>`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[--><div>true</div>-true-<!--]-->` +
`<!--if-->` +
`<span></span>` +
`</div>`,
)
data.value = false
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[--><!--]-->` +
`<!--${anchorLabel}-->` +
`<span></span>` +
`</div>`,
)
})
})
test('consecutive v-if on fragment component with anchor insertion', async () => {
runWithEnv(isProd, async () => {
const data = ref(true)
@ -1311,7 +1378,168 @@ describe('Vapor Mode hydration', () => {
}
})
test.todo('for')
describe('for', () => {
test('basic v-for', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span v-for="item in data" :key="item">{{ item }}</span>
</div>
</template>`,
undefined,
ref(['a', 'b', 'c']),
)
expect(container.innerHTML).toBe(
`<div>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`</div>`,
)
data.value.push('d')
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`</div>`,
)
})
test('v-for with text node', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span v-for="item in data" :key="item">{{ item }}</span>
</div>
</template>`,
undefined,
ref(['a', 'b', 'c']),
)
expect(container.innerHTML).toBe(
`<div><!--[--><span>a</span><span>b</span><span>c</span><!--]--></div>`,
)
data.value.push('d')
await nextTick()
expect(container.innerHTML).toBe(
`<div><!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]--></div>`,
)
})
test('v-for with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<span v-for="item in data" :key="item">{{ item }}</span>
<span/>
</div>
</template>`,
undefined,
ref(['a', 'b', 'c']),
)
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<span></span>` +
`</div>`,
)
data.value.push('d')
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<span></span>` +
`</div>`,
)
})
test('consecutive v-for with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<span v-for="item in data" :key="item">{{ item }}</span>
<span v-for="item in data" :key="item">{{ item }}</span>
<span/>
</div>
</template>`,
undefined,
ref(['a', 'b', 'c']),
)
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--[[-->` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<!--]-->` +
`<!--]]-->` +
`<span></span>` +
`</div>`,
)
data.value.push('d')
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<span></span>` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<!--[[-->` +
`<!--[-->` +
`<span>a</span>` +
`<span>b</span>` +
`<span>c</span>` +
`<span>d</span>` +
`<!--]-->` +
`<!--]]-->` +
`<span></span>` +
`</div>`,
)
})
// TODO wait for slots hydration support
test.todo('v-for on component', async () => {})
// TODO wait for slots hydration support
test.todo('on fragment component', async () => {})
// TODO wait for vapor TransitionGroup support
// v-for inside TransitionGroup does not render as a fragment
test.todo('v-for in TransitionGroup', async () => {})
})
test.todo('slots')

View File

@ -9,8 +9,14 @@ import {
shallowRef,
toReactive,
} from '@vue/reactivity'
import { getSequence, isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
import {
FOR_ANCHOR_LABEL,
getSequence,
isArray,
isObject,
isString,
} from '@vue/shared'
import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
import {
type Block,
VaporFragment,
@ -22,8 +28,17 @@ import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import { isHydrating, locateHydrationNode } from './dom/hydration'
import { insertionAnchor, insertionParent } from './insertionState'
import {
currentHydrationNode,
isComment,
isHydrating,
locateHydrationNode,
} from './dom/hydration'
import {
insertionAnchor,
insertionParent,
resetInsertionState,
} from './insertionState'
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
@ -71,15 +86,24 @@ export const createFor = (
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
locateHydrationNode()
locateHydrationNode(true)
} else {
resetInsertionState()
}
let isMounted = false
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
// TODO handle this in hydration
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const parentAnchor = isHydrating
? // Use fragment end anchor if available, otherwise use the specific for anchor.
nextSiblingAnchor(
currentHydrationNode!,
isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
)!
: __DEV__
? createComment('for')
: createTextNode()
const frag = new VaporFragment(oldBlocks)
const instance = currentInstance!
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE

View File

@ -10,7 +10,6 @@ import {
disableHydrationNodeLookup,
enableHydrationNodeLookup,
next,
prev,
} from './node'
import { isDynamicFragmentEndAnchor } from '@vue/shared'
@ -98,7 +97,7 @@ function locateHydrationNodeImpl(isFragment?: boolean) {
// if the last child is a comment, it is the anchor for the fragment
// so it need to find the previous node
if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
let previous = prev(node)
let previous = node.previousSibling //prev(node)
if (previous) node = previous
}

View File

@ -105,6 +105,7 @@ export function disableHydrationNodeLookup(): void {
}
/*! #__NO_SIDE_EFFECTS__ */
// TODO check if this is still needed
export function prev(node: Node): Node | null {
// process dynamic node (<!--[[-->...<!--]]-->) as a single one
if (isComment(node, DYNAMIC_END_ANCHOR_LABEL)) {
@ -145,6 +146,9 @@ export function nextSiblingAnchor(
anchorLabel: string,
): Comment | null {
node = handleWrappedNode(node)
if (isComment(node, anchorLabel)) {
return node as Comment
}
let n = node.nextSibling
while (n) {