mirror of https://github.com/vuejs/core.git
chore: tweaks
This commit is contained in:
parent
a9496dedb8
commit
d776a26d94
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue