wip: update
ci / continuous-release (push) Has been skipped Details
ci / test (push) Failing after 14m54s Details

This commit is contained in:
daiwei 2025-04-28 15:32:23 +08:00
parent e5399c3418
commit 4253b0ce3e
8 changed files with 125 additions and 70 deletions

View File

@ -1963,8 +1963,56 @@ describe('Vapor Mode hydration', () => {
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->hi</div>"`,
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`hi` +
`</div>`,
)
data.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`bar` +
`</div>`,
)
})
test('mixed root slot and text node', async () => {
const data = reactive({
text: 'foo',
msg: 'hi',
})
const { container } = await testHydration(
`<template>
<components.Child>
<span>{{data.text}}</span>
</components.Child>
</template>`,
{
Child: `<template>{{data.text}}<slot/>{{data.msg}}</template>`,
},
data,
)
expect(container.innerHTML).toBe(
`<!--[-->` +
`foo` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`hi` +
`<!--]-->`,
)
data.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<!--[-->` +
`foo` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`bar` +
`<!--]-->`,
)
})
@ -1985,8 +2033,20 @@ describe('Vapor Mode hydration', () => {
data,
)
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}--><div>hi</div></div>"`,
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<div>hi</div>` +
`</div>`,
)
data.msg = 'bar'
await nextTick()
expect(container.innerHTML).toBe(
`<div>` +
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
`<div>bar</div>` +
`</div>`,
)
})
@ -2024,6 +2084,7 @@ describe('Vapor Mode hydration', () => {
`<div>bar</div>` +
`</div>`,
)
data.msg2 = 'hello'
await nextTick()
expect(container.innerHTML).toBe(

View File

@ -22,7 +22,10 @@ export function createDynamicComponent(
const _insertionAnchor = insertionAnchor
if (!isHydrating) resetInsertionState()
const frag = new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
const frag =
isHydrating || __DEV__
? new DynamicFragment(DYNAMIC_COMPONENT_ANCHOR_LABEL)
: new DynamicFragment()
renderEffect(() => {
const value = getter()
frag.update(

View File

@ -16,11 +16,7 @@ import {
isObject,
isString,
} from '@vue/shared'
import {
createComment,
createTextNode,
findVaporFragmentAnchor,
} from './dom/node'
import { createComment, createTextNode } from './dom/node'
import {
type Block,
VaporFragment,
@ -34,6 +30,7 @@ import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import {
currentHydrationNode,
findVaporFragmentAnchor,
isHydrating,
locateHydrationNode,
} from './dom/hydration'

View File

@ -22,7 +22,10 @@ export function createIf(
if (once) {
frag = condition() ? b1() : b2 ? b2() : []
} else {
frag = new DynamicFragment(IF_ANCHOR_LABEL)
frag =
isHydrating || __DEV__
? new DynamicFragment(IF_ANCHOR_LABEL)
: new DynamicFragment()
renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
}

View File

@ -5,14 +5,11 @@ import {
mountComponent,
unmountComponent,
} from './component'
import {
createComment,
createTextNode,
findVaporFragmentAnchor,
} from './dom/node'
import { createComment, createTextNode } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import {
currentHydrationNode,
findVaporFragmentAnchor,
isComment,
isHydrating,
locateHydrationNode,
@ -92,13 +89,11 @@ export class DynamicFragment extends VaporFragment {
}
hydrate(label: string): void {
// for v-if="false" the hydrationNode will be a empty comment node
// use it as anchor.
// otherwise, use the next sibling comment node as anchor
// for `v-if="false"` the node will be an empty comment node use it as the anchor.
// otherwise, find next sibling vapor fragment anchor
if (isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode
} else {
// find next sibling dynamic fragment end anchor
const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)!
if (anchor) {
this.anchor = anchor

View File

@ -124,7 +124,10 @@ export function createSlot(
fallback,
)
} else {
fragment = new DynamicFragment(SLOT_ANCHOR_LABEL)
fragment =
isHydrating || __DEV__
? new DynamicFragment(SLOT_ANCHOR_LABEL)
: new DynamicFragment()
const isDynamicName = isFunction(name)
const renderSlot = () => {
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)

View File

@ -11,7 +11,7 @@ import {
enableHydrationNodeLookup,
next,
} from './node'
import { isVaporFragmentEndAnchor } from '@vue/shared'
import { isDynamicAnchor, isVaporFragmentEndAnchor } from '@vue/shared'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
@ -191,3 +191,29 @@ export function locateEndAnchor(
}
return null
}
export function isNonHydrationNode(node: Node): boolean {
return (
// empty text nodes
isEmptyText(node) ||
// dynamic node anchors (<!--[[-->, <!--]]-->)
isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`)
isComment(node, ']') ||
// vapor fragment end anchors
isVaporFragmentEndAnchor(node)
)
}
export function findVaporFragmentAnchor(
node: Node,
anchorLabel: string,
): Comment | null {
let n = node.nextSibling
while (n) {
if (isComment(n, anchorLabel)) return n
n = n.nextSibling
}
return null
}

View File

@ -1,8 +1,7 @@
import { isComment, isEmptyText, locateEndAnchor } from './hydration'
import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
import {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
isDynamicAnchor,
isVaporFragmentEndAnchor,
} from '@vue/shared'
@ -42,7 +41,7 @@ export function __child(node: ParentNode): Node {
* _setInsertionState(n2, 0) -> slot fragment
*
* during hydration:
* const n2 = _template("<div><!--[-->slot<!--]--><!--slot-->Hi</div>")()
* const n2 = <div><!--[-->slot<!--]--><!--slot-->Hi</div> // server output
* const n1 = _child(n2) -> should be `Hi` instead of the slot fragment
* _setInsertionState(n2, 0) -> slot fragment
*/
@ -79,7 +78,19 @@ function _next(node: Node): Node {
/*! #__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node {
node = handleWrappedNode(node)
// process dynamic node (<!--[[-->...<!--]]-->) as a single node
if (isComment(node, DYNAMIC_START_ANCHOR_LABEL)) {
node = locateEndAnchor(
node,
DYNAMIC_START_ANCHOR_LABEL,
DYNAMIC_END_ANCHOR_LABEL,
)!
}
// process fragment (<!--[-->...<!--]-->) as a single node
else if (isComment(node, '[')) {
node = locateEndAnchor(node)!
}
let n = node.nextSibling!
while (n && isNonHydrationNode(n)) {
@ -142,47 +153,3 @@ export function disableHydrationNodeLookup(): void {
next.impl = _next
nthChild.impl = _nthChild
}
function isNonHydrationNode(node: Node) {
return (
// empty text nodes, no need to hydrate
isEmptyText(node) ||
// dynamic node anchors (<!--[[-->, <!--]]-->)
isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`)
isComment(node, ']') ||
// vapor fragment end anchors
isVaporFragmentEndAnchor(node)
)
}
export function findVaporFragmentAnchor(
node: Node,
anchorLabel: string,
): Comment | null {
let n = node.nextSibling
while (n) {
if (isComment(n, anchorLabel)) return n
n = n.nextSibling
}
return null
}
function handleWrappedNode(node: Node): Node {
// process dynamic node (<!--[[-->...<!--]]-->) as a single one
if (isComment(node, DYNAMIC_START_ANCHOR_LABEL)) {
return locateEndAnchor(
node,
DYNAMIC_START_ANCHOR_LABEL,
DYNAMIC_END_ANCHOR_LABEL,
)!
}
// process fragment (<!--[-->...<!--]-->) as a single one
else if (isComment(node, '[')) {
return locateEndAnchor(node)!
}
return node
}