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, data,
) )
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toBe(
`"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->hi</div>"`, `<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, data,
) )
expect(container.innerHTML).toMatchInlineSnapshot( expect(container.innerHTML).toBe(
`"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}--><div>hi</div></div>"`, `<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>bar</div>` +
`</div>`, `</div>`,
) )
data.msg2 = 'hello' data.msg2 = 'hello'
await nextTick() await nextTick()
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import {
enableHydrationNodeLookup, enableHydrationNodeLookup,
next, next,
} from './node' } from './node'
import { isVaporFragmentEndAnchor } from '@vue/shared' import { isDynamicAnchor, isVaporFragmentEndAnchor } from '@vue/shared'
export let isHydrating = false export let isHydrating = false
export let currentHydrationNode: Node | null = null export let currentHydrationNode: Node | null = null
@ -191,3 +191,29 @@ export function locateEndAnchor(
} }
return null 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 { import {
DYNAMIC_END_ANCHOR_LABEL, DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL, DYNAMIC_START_ANCHOR_LABEL,
isDynamicAnchor,
isVaporFragmentEndAnchor, isVaporFragmentEndAnchor,
} from '@vue/shared' } from '@vue/shared'
@ -42,7 +41,7 @@ export function __child(node: ParentNode): Node {
* _setInsertionState(n2, 0) -> slot fragment * _setInsertionState(n2, 0) -> slot fragment
* *
* during hydration: * 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 * const n1 = _child(n2) -> should be `Hi` instead of the slot fragment
* _setInsertionState(n2, 0) -> slot fragment * _setInsertionState(n2, 0) -> slot fragment
*/ */
@ -79,7 +78,19 @@ function _next(node: Node): Node {
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node { 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! let n = node.nextSibling!
while (n && isNonHydrationNode(n)) { while (n && isNonHydrationNode(n)) {
@ -142,47 +153,3 @@ export function disableHydrationNodeLookup(): void {
next.impl = _next next.impl = _next
nthChild.impl = _nthChild 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
}