wip: save

This commit is contained in:
daiwei 2025-04-23 14:23:09 +08:00
parent 3108d91dfa
commit 1248172216
3 changed files with 163 additions and 20 deletions

View File

@ -264,7 +264,7 @@ describe('Vapor Mode hydration', () => {
) )
}) })
test('consecutive component with anchor insertion', async () => { test('consecutive components with anchor insertion', async () => {
const { container, data } = await testHydration( const { container, data } = await testHydration(
`<template> `<template>
<div> <div>
@ -344,6 +344,111 @@ describe('Vapor Mode hydration', () => {
) )
}) })
test('fragment component with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<components.Child/>
<span/>
</div>
</template>
`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><div>foo</div>-foo<!--]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><div>bar</div>-bar<!--]--><span></span></div>"`,
)
})
test('consecutive fragment components with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<components.Child/>
<components.Child/>
<span/>
</div>
</template>
`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><span></span></div>"`,
)
})
test('mixed fragment component and element with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<components.Child/>
<span/>
<components.Child/>
<span/>
</div>
</template>
`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><div>foo</div>-foo<!--]--><span></span><!--[--><div>foo</div>-foo<!--]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[--><div>bar</div>-bar<!--]--><span></span><!--[--><div>bar</div>-bar<!--]--><span></span></div>"`,
)
})
test('mixed fragment component and text with anchor insertion', async () => {
const { container, data } = await testHydration(
`<template>
<div>
<span/>
<components.Child/>
{{ data }}
<components.Child/>
<span/>
</div>
</template>
`,
{
Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
},
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><!--[[--> <!--]]--><!--[[--> foo <!--]]--><!--[[--> <!--]]--><!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]--><span></span></div>"`,
)
data.value = 'bar'
await nextTick()
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><span></span><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><!--[[--> <!--]]--><!--[[--> bar <!--]]--><!--[[--> <!--]]--><!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]--><span></span></div>"`,
)
})
test.todo('if') test.todo('if')
test.todo('for') test.todo('for')

View File

@ -46,7 +46,7 @@ export const isComment = (node: Node, data: string): node is Anchor =>
*/ */
function adoptTemplateImpl(node: Node, template: string): Node | null { function adoptTemplateImpl(node: Node, template: string): Node | null {
if (!(template[0] === '<' && template[1] === '!')) { if (!(template[0] === '<' && template[1] === '!')) {
while (node.nodeType === 8) node = next(node) while (node.nodeType === 8) node = node.nextSibling!
} }
if (__DEV__) { if (__DEV__) {
@ -119,3 +119,33 @@ function locateHydrationNodeImpl() {
resetInsertionState() resetInsertionState()
currentHydrationNode = node currentHydrationNode = node
} }
export function isDynamicAnchor(node: Node): node is Comment {
return isComment(node, '[[') || isComment(node, ']]')
}
export function isEmptyText(node: Node): node is Text {
return node.nodeType === 3 && !(node as Text).data.trim()
}
export function locateEndAnchor(
node: Node | null,
open = '[',
close = ']',
): Node | null {
let match = 0
while (node) {
node = node.nextSibling
if (node && node.nodeType === 8) {
if ((node as Comment).data === open) match++
if ((node as Comment).data === close) {
if (match === 0) {
return node
} else {
match--
}
}
}
}
return null
}

View File

@ -1,7 +1,12 @@
import {
isComment,
isDynamicAnchor,
isEmptyText,
isHydrating,
locateEndAnchor,
} from './hydration'
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
import { isComment, isHydrating } from './hydration'
export function createTextNode(value = ''): Text { export function createTextNode(value = ''): Text {
return document.createTextNode(value) return document.createTextNode(value)
} }
@ -23,25 +28,28 @@ export function child(node: ParentNode): Node {
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function nthChild(node: Node, i: number): Node { export function nthChild(node: Node, i: number): Node {
return node.childNodes[i] if (!isHydrating) return node.childNodes[i]
}
/*! #__NO_SIDE_EFFECTS__ */ let n = node.firstChild!
export function next(node: Node): Node { for (let start = 0; start < i; start++) {
let n = node.nextSibling! n = next(n) as ChildNode
if (isHydrating) {
// skip dynamic anchors and empty text nodes
while (n && (isDynamicAnchor(n) || isEmptyText(n))) {
n = n.nextSibling!
}
} }
return n return n
} }
function isDynamicAnchor(node: Node): node is Comment { /*! #__NO_SIDE_EFFECTS__ */
return isComment(node, '[[') || isComment(node, ']]') export function next(node: Node): Node {
} if (!isHydrating) return node.nextSibling!
function isEmptyText(node: Node): node is Text { // process fragment as a single node
return node.nodeType === 3 && !(node as Text).data.trim() if (node && isComment(node, '[')) {
node = locateEndAnchor(node)!
}
let n = node.nextSibling!
// skip dynamic anchors and empty text nodes
while (n && (isDynamicAnchor(n) || isEmptyText(n))) {
n = n.nextSibling!
}
return n
} }