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) {
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) {
return prevDynamicCount > 0 && nextDynamicCount > 0
}

View File

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

View File

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

View File

@ -30,9 +30,9 @@ import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
import {
currentHydrationNode,
findVaporFragmentAnchor,
isHydrating,
locateHydrationNode,
locateVaporFragmentAnchor,
} from './dom/hydration'
import {
insertionAnchor,
@ -97,13 +97,13 @@ export const createFor = (
let parent: ParentNode | undefined | null
let parentAnchor: Node
if (isHydrating) {
parentAnchor = findVaporFragmentAnchor(
parentAnchor = locateVaporFragmentAnchor(
currentHydrationNode!,
FOR_ANCHOR_LABEL,
)!
if (__DEV__ && !parentAnchor) {
// TODO warn, should not happen
warn(`createFor anchor not found...`)
// this should not happen
throw new Error(`v-for fragment anchor node was not found.`)
}
} else {
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 {
currentHydrationNode,
findVaporFragmentAnchor,
isComment,
isHydrating,
locateHydrationNode,
locateVaporFragmentAnchor,
} from './dom/hydration'
import { warn } from '@vue/runtime-dom'
export type Block =
| Node
@ -89,17 +88,17 @@ export class DynamicFragment extends VaporFragment {
}
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
if (isComment(currentHydrationNode!, '')) {
this.anchor = currentHydrationNode
} else {
const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)!
const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
if (anchor) {
this.anchor = anchor
} else if (__DEV__) {
// TODO warning, should not happen
warn(`DynamicFragment anchor not found...`)
// this should not happen
throw new Error(`${label} fragment anchor node was not found.`)
}
}
}

View File

@ -6,12 +6,11 @@ import {
setInsertionState,
} from '../insertionState'
import {
_child,
disableHydrationNodeLookup,
enableHydrationNodeLookup,
next,
} from './node'
import { isDynamicAnchor, isVaporFragmentEndAnchor } from '@vue/shared'
import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
@ -33,7 +32,6 @@ function performHydration<T>(
// optimize anchor cache lookup
;(Comment.prototype as any).$fs = undefined
;(Node.prototype as any).$nc = undefined
isOptimized = true
}
enableHydrationNodeLookup()
@ -101,24 +99,29 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
return node
}
const hydrationPositionMap = new WeakMap<ParentNode, Node>()
function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
let node: Node | null
// prepend / firstChild
if (insertionAnchor === 0) {
node = _child(insertionParent!)
node = insertionParent!.firstChild
} 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
} else {
node = insertionParent
? insertionParent.$nc || insertionParent.lastChild
? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
: currentHydrationNode
// if the last child is a vapor fragment end anchor, find the previous one
if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
// if node is a vapor fragment anchor, find the previous one
if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
node = node.previousSibling
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) {
insertionParent.$nc = node!.previousSibling
const prev = node.previousSibling
if (prev) hydrationPositionMap.set(insertionParent, prev)
}
}
@ -166,10 +170,6 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
currentHydrationNode = 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 = '[',
@ -194,26 +194,26 @@ export function locateEndAnchor(
export function isNonHydrationNode(node: Node): boolean {
return (
// empty text nodes
isEmptyText(node) ||
// dynamic node anchors (<!--[[-->, <!--]]-->)
isDynamicAnchor(node) ||
// fragment end anchor (`<!--]-->`)
// empty text node
isEmptyTextNode(node) ||
// vdom fragment end anchor (`<!--]-->`)
isComment(node, ']') ||
// vapor fragment end anchors
isVaporFragmentEndAnchor(node)
// vapor mode specific anchors
isVaporAnchors(node)
)
}
export function findVaporFragmentAnchor(
export function locateVaporFragmentAnchor(
node: Node,
anchorLabel: string,
): Comment | null {
): Comment | undefined {
let n = node.nextSibling
while (n) {
if (isComment(n, anchorLabel)) return n
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 {
DYNAMIC_END_ANCHOR_LABEL,
DYNAMIC_START_ANCHOR_LABEL,
isVaporFragmentEndAnchor,
isVaporAnchors,
} from '@vue/shared'
/*! #__NO_SIDE_EFFECTS__ */
@ -25,33 +25,43 @@ export function _child(node: ParentNode): Node {
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__ */
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!
if (isComment(n, '[')) {
n = locateEndAnchor(n)!.nextSibling!
}
while (n && isVaporFragmentEndAnchor(n)) {
while (n && isVaporAnchors(n)) {
n = n.nextSibling!
}
return n
@ -62,11 +72,14 @@ export function _nthChild(node: Node, i: number): Node {
return node.childNodes[i]
}
/**
* Hydration-specific version of `nthChild`.
*/
/*! #__NO_SIDE_EFFECTS__ */
export function __nthChild(node: Node, i: number): Node {
let n = node.firstChild!
for (let start = 0; start < i; start++) {
n = next(n) as ChildNode
n = __next(n) as ChildNode
}
return n
}
@ -76,6 +89,46 @@ function _next(node: Node): Node {
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__ */
export function __next(node: Node): Node {
// process dynamic node (<!--[[-->...<!--]]-->) as a single node
@ -99,49 +152,36 @@ export function __next(node: Node): Node {
return n
}
type ChildFn = (node: ParentNode) => Node
type NextFn = (node: Node) => Node
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
type DelegatedFunction<T extends (...args: any[]) => any> = T & {
impl: T
}
/*! #__NO_SIDE_EFFECTS__ */
export const child: DelegatedChildFunction = node => {
export const child: DelegatedFunction<typeof _child> = node => {
return child.impl(node)
}
child.impl = _child
/*! #__NO_SIDE_EFFECTS__ */
export const next: DelegatedNextFunction = node => {
export const next: DelegatedFunction<typeof _next> = node => {
return next.impl(node)
}
next.impl = _next
/*! #__NO_SIDE_EFFECTS__ */
export const nthChild: DelegatedNthChildFunction = (node, i) => {
export const nthChild: DelegatedFunction<typeof _nthChild> = (node, i) => {
return nthChild.impl(node, i)
}
nthChild.impl = _nthChild
// During hydration, there might be differences between the server-rendered (SSR)
// HTML and the client-side template.
// For example, a dynamic node `<!>` in the template might be rendered as a
// `Fragment` (`<!--[-->...<!--]-->`) in the SSR output.
// The content of the `Fragment` affects the lookup results of the `next` and
// `nthChild` functions.
// To ensure the hydration process correctly finds nodes, we need to treat the
// `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.
/**
* Enables hydration-specific node lookup behavior.
*
* Temporarily switches the implementations of the exported
* `child`, `next`, and `nthChild` functions to their hydration-specific
* versions (`__child`, `__next`, `__nthChild`). This allows traversal
* logic to correctly handle SSR comment anchors during hydration.
*/
export function enableHydrationNodeLookup(): void {
child.impl = __child
next.impl = __next

View File

@ -1,9 +1,4 @@
export let insertionParent:
| (ParentNode & {
// the next child node to be hydrated
$nc?: Node | null
})
| undefined
export let insertionParent: ParentNode | 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
const data = (node as Comment).data
@ -26,3 +26,7 @@ export function isVaporFragmentEndAnchor(node: Node): node is Comment {
data === DYNAMIC_COMPONENT_ANCHOR_LABEL
)
}
export function isVaporAnchors(node: Node): node is Comment {
return isDynamicAnchor(node) || isVaporFragmentAnchor(node)
}