chore: tweaks
ci / test (push) Has been cancelled Details
ci / continuous-release (push) Has been cancelled Details

This commit is contained in:
daiwei 2025-04-30 08:33:57 +08:00
parent a9496dedb8
commit d776a26d94
9 changed files with 134 additions and 98 deletions

View File

@ -410,7 +410,7 @@ function shouldProcessChildAsDynamic(
if (dynamicNodeCount === 2) { if (dynamicNodeCount === 2) {
return prevDynamicCount > 0 return prevDynamicCount > 0
} }
// For three or more dynamic nodes, mark the intermediate node as dynamic // For three or more dynamic nodes, mark the middle nodes as dynamic
else if (dynamicNodeCount >= 3) { else if (dynamicNodeCount >= 3) {
return prevDynamicCount > 0 && nextDynamicCount > 0 return prevDynamicCount > 0 && nextDynamicCount > 0
} }

View File

@ -25,14 +25,13 @@ import {
getEscapedCssVarName, getEscapedCssVarName,
includeBooleanAttr, includeBooleanAttr,
isBooleanAttr, isBooleanAttr,
isDynamicAnchor,
isKnownHtmlAttr, isKnownHtmlAttr,
isKnownSvgAttr, isKnownSvgAttr,
isOn, isOn,
isRenderableAttrValue, isRenderableAttrValue,
isReservedProp, isReservedProp,
isString, isString,
isVaporFragmentEndAnchor, isVaporAnchors,
normalizeClass, normalizeClass,
normalizeStyle, normalizeStyle,
stringifyStyle, stringifyStyle,
@ -127,10 +126,8 @@ export function createHydrationFunctions(
function nextSibling(node: Node) { function nextSibling(node: Node) {
let n = next(node) let n = next(node)
// skip if: // skip vapor mode specific anchors
// - dynamic anchors (`<!--[[-->`, `<!--][-->`) if (n && isVaporAnchors(n)) {
// - vapor fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
n = next(n) n = next(n)
} }
return n return n
@ -162,7 +159,8 @@ export function createHydrationFunctions(
slotScopeIds: string[] | null, slotScopeIds: string[] | null,
optimized = false, optimized = false,
): Node | null => { ): Node | null => {
if (isDynamicAnchor(node) || isVaporFragmentEndAnchor(node)) { // skip vapor mode specific anchors
if (isVaporAnchors(node)) {
node = nextSibling(node)! node = nextSibling(node)!
} }
optimized = optimized || !!vnode.dynamicChildren optimized = optimized || !!vnode.dynamicChildren

View File

@ -107,7 +107,7 @@ export interface Renderer<HostElement = RendererElement> {
export interface HydrationRenderer extends Renderer<Element | ShadowRoot> { export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
hydrate: RootHydrateFunction hydrate: RootHydrateFunction
hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
} }
export type ElementNamespace = 'svg' | 'mathml' | undefined export type ElementNamespace = 'svg' | 'mathml' | undefined

View File

@ -30,9 +30,9 @@ 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,
locateVaporFragmentAnchor,
} from './dom/hydration' } from './dom/hydration'
import { import {
insertionAnchor, insertionAnchor,
@ -97,13 +97,13 @@ export const createFor = (
let parent: ParentNode | undefined | null let parent: ParentNode | undefined | null
let parentAnchor: Node let parentAnchor: Node
if (isHydrating) { if (isHydrating) {
parentAnchor = findVaporFragmentAnchor( parentAnchor = locateVaporFragmentAnchor(
currentHydrationNode!, currentHydrationNode!,
FOR_ANCHOR_LABEL, FOR_ANCHOR_LABEL,
)! )!
if (__DEV__ && !parentAnchor) { if (__DEV__ && !parentAnchor) {
// TODO warn, should not happen // this should not happen
warn(`createFor anchor not found...`) throw new Error(`v-for fragment anchor node was not found.`)
} }
} else { } else {
parentAnchor = __DEV__ ? createComment('for') : createTextNode() parentAnchor = __DEV__ ? createComment('for') : createTextNode()

View File

@ -9,12 +9,11 @@ import { createComment, createTextNode } 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,
locateVaporFragmentAnchor,
} from './dom/hydration' } from './dom/hydration'
import { warn } from '@vue/runtime-dom'
export type Block = export type Block =
| Node | Node
@ -89,17 +88,17 @@ export class DynamicFragment extends VaporFragment {
} }
hydrate(label: string): void { hydrate(label: string): void {
// for `v-if="false"` the node will be an empty comment node use it as the anchor. // for `v-if="false"` the node will be an empty comment, use it as the anchor.
// otherwise, find next sibling vapor fragment anchor // otherwise, find next sibling vapor fragment anchor
if (isComment(currentHydrationNode!, '')) { if (isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode this.anchor = currentHydrationNode
} else { } else {
const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)! const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
if (anchor) { if (anchor) {
this.anchor = anchor this.anchor = anchor
} else if (__DEV__) { } else if (__DEV__) {
// TODO warning, should not happen // this should not happen
warn(`DynamicFragment anchor not found...`) throw new Error(`${label} fragment anchor node was not found.`)
} }
} }
} }

View File

@ -6,12 +6,11 @@ import {
setInsertionState, setInsertionState,
} from '../insertionState' } from '../insertionState'
import { import {
_child,
disableHydrationNodeLookup, disableHydrationNodeLookup,
enableHydrationNodeLookup, enableHydrationNodeLookup,
next, next,
} from './node' } from './node'
import { isDynamicAnchor, isVaporFragmentEndAnchor } from '@vue/shared' import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
export let isHydrating = false export let isHydrating = false
export let currentHydrationNode: Node | null = null export let currentHydrationNode: Node | null = null
@ -33,7 +32,6 @@ function performHydration<T>(
// optimize anchor cache lookup // optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined ;(Comment.prototype as any).$fs = undefined
;(Node.prototype as any).$nc = undefined
isOptimized = true isOptimized = true
} }
enableHydrationNodeLookup() enableHydrationNodeLookup()
@ -101,24 +99,29 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
return node return node
} }
const hydrationPositionMap = new WeakMap<ParentNode, Node>()
function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) { function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
let node: Node | null let node: Node | null
// prepend / firstChild // prepend / firstChild
if (insertionAnchor === 0) { if (insertionAnchor === 0) {
node = _child(insertionParent!) node = insertionParent!.firstChild
} else if (insertionAnchor) { } else if (insertionAnchor) {
// for dynamic children, use insertionAnchor as the node // `insertionAnchor` is a Node, it is the DOM node to hydrate
// Template: `...<span/><!----><span/>...`// `insertionAnchor` is the placeholder
// SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
node = insertionAnchor node = insertionAnchor
} else { } else {
node = insertionParent node = insertionParent
? insertionParent.$nc || insertionParent.lastChild ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
: currentHydrationNode : currentHydrationNode
// if the last child is a vapor fragment end anchor, find the previous one // if node is a vapor fragment anchor, find the previous one
if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) { if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
node = node.previousSibling node = node.previousSibling
if (__DEV__ && !node) { if (__DEV__ && !node) {
// TODO warning, should not happen // this should not happen
throw new Error(`vapor fragment anchor previous node was not found.`)
} }
} }
@ -153,7 +156,8 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
} }
if (insertionParent && node) { if (insertionParent && node) {
insertionParent.$nc = node!.previousSibling const prev = node.previousSibling
if (prev) hydrationPositionMap.set(insertionParent, prev)
} }
} }
@ -166,10 +170,6 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
currentHydrationNode = node currentHydrationNode = node
} }
export function isEmptyText(node: Node): node is Text {
return node.nodeType === 3 && !(node as Text).data.trim()
}
export function locateEndAnchor( export function locateEndAnchor(
node: Node | null, node: Node | null,
open = '[', open = '[',
@ -194,26 +194,26 @@ export function locateEndAnchor(
export function isNonHydrationNode(node: Node): boolean { export function isNonHydrationNode(node: Node): boolean {
return ( return (
// empty text nodes // empty text node
isEmptyText(node) || isEmptyTextNode(node) ||
// dynamic node anchors (<!--[[-->, <!--]]-->) // vdom fragment end anchor (`<!--]-->`)
isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`)
isComment(node, ']') || isComment(node, ']') ||
// vapor fragment end anchors // vapor mode specific anchors
isVaporFragmentEndAnchor(node) isVaporAnchors(node)
) )
} }
export function findVaporFragmentAnchor( export function locateVaporFragmentAnchor(
node: Node, node: Node,
anchorLabel: string, anchorLabel: string,
): Comment | null { ): Comment | undefined {
let n = node.nextSibling let n = node.nextSibling
while (n) { while (n) {
if (isComment(n, anchorLabel)) return n if (isComment(n, anchorLabel)) return n
n = n.nextSibling n = n.nextSibling
} }
}
return null
export function isEmptyTextNode(node: Node): node is Text {
return node.nodeType === 3 && !(node as Text).data.trim()
} }

View File

@ -2,7 +2,7 @@ import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
import { import {
DYNAMIC_END_ANCHOR_LABEL, DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL, DYNAMIC_START_ANCHOR_LABEL,
isVaporFragmentEndAnchor, isVaporAnchors,
} from '@vue/shared' } from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
@ -25,33 +25,43 @@ export function _child(node: ParentNode): Node {
return node.firstChild! return node.firstChild!
} }
/**
* Hydration-specific version of `child`.
*
* This function skips leading fragment anchors to find the first node relevant
* for hydration matching against the client-side template structure.
*
* Problem:
* Template: `<div><slot />{{ msg }}</div>`
*
* Client Compiled Code (Simplified):
* const n2 = t0() // n2 = `<div> </div>`
* const n1 = _child(n2) // n1 = text node
* // ... slot creation ...
* _renderEffect(() => _setText(n1, _ctx.msg))
*
* SSR Output: `<div><!--[-->slot content<!--]-->Actual Text Node</div>`
*
* Hydration Mismatch:
* - During hydration, `n2` refers to the SSR `<div>`.
* - `_child(n2)` would return `<!--[-->`.
* - The client code expects `n1` to be the text node, but gets the comment.
* The subsequent `_setText(n1, ...)` would fail or target the wrong node.
*
* Solution (`__child`):
* - `__child(n2)` is used during hydration. It skips the SSR fragment anchors
* (`<!--[-->...<!--]-->`) and any other non-content nodes to find the
* "Actual Text Node", correctly matching the client's expectation for `n1`.
*/
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __child(node: ParentNode): Node { export function __child(node: ParentNode): Node {
/**
* During hydration, the first child of a node not be the expected
* if the first child is slot
*
* for template code: `div><slot />{{ data }}</div>`
* - slot: 'slot',
* - data: 'hi',
*
* client side:
* const n2 = _template("<div> </div>")()
* const n1 = _child(n2) -> the text node
* _setInsertionState(n2, 0) -> slot fragment
*
* during hydration:
* 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
*/
let n = node.firstChild! let n = node.firstChild!
if (isComment(n, '[')) { if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling! n = locateEndAnchor(n)!.nextSibling!
} }
while (n && isVaporFragmentEndAnchor(n)) { while (n && isVaporAnchors(n)) {
n = n.nextSibling! n = n.nextSibling!
} }
return n return n
@ -62,11 +72,14 @@ export function _nthChild(node: Node, i: number): Node {
return node.childNodes[i] return node.childNodes[i]
} }
/**
* Hydration-specific version of `nthChild`.
*/
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node { export function __nthChild(node: Node, i: number): Node {
let n = node.firstChild! let n = node.firstChild!
for (let start = 0; start < i; start++) { for (let start = 0; start < i; start++) {
n = next(n) as ChildNode n = __next(n) as ChildNode
} }
return n return n
} }
@ -76,6 +89,46 @@ function _next(node: Node): Node {
return node.nextSibling! return node.nextSibling!
} }
/**
* Hydration-specific version of `next`.
*
* SSR comment anchors (fragments `<!--[-->...<!--]-->`, dynamic `<!--[[-->...<!--]]-->`)
* disrupt standard `node.nextSibling` traversal during hydration. `_next` might
* return a comment node or an internal node of a fragment instead of skipping
* the entire fragment block.
*
* Example:
* Template: `<div>Node1<!>Node2</div>` (where <!> is a dynamic component placeholder)
*
* Client Compiled Code (Simplified):
* const n2 = t0() // n2 = `<div>Node1<!---->Node2</div>`
* const n1 = _next(_child(n2)) // n1 = _next(Node1) returns `<!---->`
* _setInsertionState(n2, n1) // insertion anchor is `<!---->`
* const n0 = _createComponent(_ctx.Comp) // inserted before `<!---->`
*
* SSR Output: `<div>Node1<!--[-->Node3 Node4<!--]-->Node2</div>`
*
* Hydration Mismatch:
* - During hydration, `n2` refers to the SSR `<div>`.
* - `_child(n2)` returns `Node1`.
* - `_next(Node1)` would return `<!--[-->`.
* - The client logic expects `n1` to be the node *after* `Node1` in its structure
* (the placeholder), but gets the fragment start anchor `<!--[-->` from SSR.
* - Using `<!--[-->` as the insertion anchor for hydrating the component is incorrect.
*
* Solution (`__next`):
* - During hydration, `next.impl` is `__next`.
* - `n1 = __next(Node1)` is called.
* - `__next` recognizes that the immediate sibling `<!--[-->` is a fragment start anchor.
* - It skips the entire fragment block (`<!--[-->Node3 Node4<!--]-->`).
* - It returns the node immediately *after* the fragment's end anchor, which is `Node2`.
* - This correctly identifies the logical "next sibling" anchor (`Node2`) in the SSR structure,
* allowing the component to be hydrated correctly relative to `Node1` and `Node2`.
*
* This function ensures traversal correctly skips over non-hydration nodes and
* treats entire fragment/dynamic blocks (when starting *from* their beginning anchor)
* as single logical units to find the next actual sibling node for hydration matching.
*/
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function __next(node: Node): Node { export function __next(node: Node): Node {
// process dynamic node (<!--[[-->...<!--]]-->) as a single node // process dynamic node (<!--[[-->...<!--]]-->) as a single node
@ -99,49 +152,36 @@ export function __next(node: Node): Node {
return n return n
} }
type ChildFn = (node: ParentNode) => Node type DelegatedFunction<T extends (...args: any[]) => any> = T & {
type NextFn = (node: Node) => Node impl: T
type NthChildFn = (node: Node, i: number) => Node
interface DelegatedChildFunction extends ChildFn {
impl: ChildFn
}
interface DelegatedNextFunction extends NextFn {
impl: NextFn
}
interface DelegatedNthChildFunction extends NthChildFn {
impl: NthChildFn
} }
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export const child: DelegatedChildFunction = node => { export const child: DelegatedFunction<typeof _child> = node => {
return child.impl(node) return child.impl(node)
} }
child.impl = _child child.impl = _child
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export const next: DelegatedNextFunction = node => { export const next: DelegatedFunction<typeof _next> = node => {
return next.impl(node) return next.impl(node)
} }
next.impl = _next next.impl = _next
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export const nthChild: DelegatedNthChildFunction = (node, i) => { export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
return nthChild.impl(node, i) return nthChild.impl(node, i)
} }
nthChild.impl = _nthChild nthChild.impl = _nthChild
// During hydration, there might be differences between the server-rendered (SSR) /**
// HTML and the client-side template. * Enables hydration-specific node lookup behavior.
// For example, a dynamic node `<!>` in the template might be rendered as a *
// `Fragment` (`<!--[-->...<!--]-->`) in the SSR output. * Temporarily switches the implementations of the exported
// The content of the `Fragment` affects the lookup results of the `next` and * `child`, `next`, and `nthChild` functions to their hydration-specific
// `nthChild` functions. * versions (`__child`, `__next`, `__nthChild`). This allows traversal
// To ensure the hydration process correctly finds nodes, we need to treat the * logic to correctly handle SSR comment anchors during hydration.
// `Fragment` as a single node. */
// Therefore, during hydration, we need to temporarily switch the implementations
// of `next` and `nthChild`. After hydration is complete, their implementations
// are restored to the original versions.
export function enableHydrationNodeLookup(): void { export function enableHydrationNodeLookup(): void {
child.impl = __child child.impl = __child
next.impl = __next next.impl = __next

View File

@ -1,9 +1,4 @@
export let insertionParent: export let insertionParent: ParentNode | undefined
| (ParentNode & {
// the next child node to be hydrated
$nc?: Node | null
})
| undefined
export let insertionAnchor: Node | 0 | undefined export let insertionAnchor: Node | 0 | undefined
/** /**

View File

@ -15,7 +15,7 @@ export function isDynamicAnchor(node: Node): node is Comment {
) )
} }
export function isVaporFragmentEndAnchor(node: Node): node is Comment { export function isVaporFragmentAnchor(node: Node): node is Comment {
if (node.nodeType !== 8) return false if (node.nodeType !== 8) return false
const data = (node as Comment).data const data = (node as Comment).data
@ -26,3 +26,7 @@ export function isVaporFragmentEndAnchor(node: Node): node is Comment {
data === DYNAMIC_COMPONENT_ANCHOR_LABEL data === DYNAMIC_COMPONENT_ANCHOR_LABEL
) )
} }
export function isVaporAnchors(node: Node): node is Comment {
return isDynamicAnchor(node) || isVaporFragmentAnchor(node)
}