Parcourir la source

refactor(runtime-vapor): simplify hydration close tracking in vapor runtime

daiwei il y a 2 semaines
Parent
commit
fa045cca24

+ 3 - 3
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -3182,7 +3182,7 @@ describe('Vapor Mode hydration', () => {
       expect(`Hydration node mismatch`).toHaveBeenWarned()
       expect(`Hydration text mismatch`).toHaveBeenWarned()
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
-        `"<div>\n<!--[--><!--]-->\n<span>foo</span><span>bar</span><!--for--><i>tail</i></div>"`,
+        `"<div>\n<!--[--><span>foo</span><span>bar</span><!--]-->\n<i>tail</i></div>"`,
       )
     })
 
@@ -5104,8 +5104,8 @@ describe('Vapor Mode hydration', () => {
       expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
       	"<div>
       	<!--[-->
-      	<!--[--><!--]-->
-      	<span>foo</span><span>bar</span><!--for--><i>tail</i><!--]-->
+      	<!--[--><span>foo</span><span>bar</span><!--]-->
+      	<i>tail</i><!--]-->
       	</div><!--async component-->"
       `)
     })

+ 10 - 35
packages/runtime-vapor/src/apiCreateFor.ts

@@ -26,14 +26,13 @@ import { VaporVForFlags } from '@vue/shared'
 import {
   advanceHydrationNode,
   currentHydrationNode,
+  enterHydrationBoundary,
   isComment,
   isHydrating,
   locateHydrationBoundaryClose,
   locateHydrationNode,
   locateNextNode,
   markHydrationAnchor,
-  patchCurrentHydrationBoundary,
-  pushHydrationBoundary,
   setCurrentHydrationNode,
 } from './dom/hydration'
 import {
@@ -124,7 +123,7 @@ export const createFor = (
       isMounted = true
       if (isHydrating) {
         const hydrationStart = currentHydrationNode!
-        const restoreBoundary = pushHydrationBoundary({})
+        let exitHydrationBoundary: (() => void) | undefined
         let nextNode
         const emptyLocalRange =
           isComment(hydrationStart, ']') &&
@@ -134,31 +133,17 @@ export const createFor = (
 
         try {
           if (emptyLocalRange && newLength) {
-            patchCurrentHydrationBoundary({ close: hydrationStart })
-            const anchor = (hydrationStart.nextSibling || hydrationStart)!
-            parentAnchor = markHydrationAnchor(
-              __DEV__ ? createComment('for') : createTextNode(),
-            )
-            patchCurrentHydrationBoundary({
-              preserve: parentAnchor,
-              cleanupOnPop: true,
-            })
-            anchor.parentNode!.insertBefore(parentAnchor, anchor)
-            setCurrentHydrationNode(parentAnchor)
+            parentAnchor = markHydrationAnchor(hydrationStart)
+            exitHydrationBoundary = enterHydrationBoundary(parentAnchor)
             for (let i = 0; i < newLength; i++) {
               mount(source, i)
             }
-            setCurrentHydrationNode(anchor)
+            setCurrentHydrationNode(parentAnchor)
           } else {
             for (let i = 0; i < newLength; i++) {
               if (isComment(currentHydrationNode!, ']')) {
-                const anchor = markHydrationAnchor(currentHydrationNode!)
-                patchCurrentHydrationBoundary({
-                  preserve: anchor,
-                  cleanupOnPop: true,
-                })
-                nextNode = anchor
-                setCurrentHydrationNode(anchor)
+                nextNode = markHydrationAnchor(currentHydrationNode!)
+                setCurrentHydrationNode(nextNode)
               } else {
                 nextNode = locateNextNode(currentHydrationNode!)
               }
@@ -169,11 +154,10 @@ export const createFor = (
             // Slot fallback can fall through an empty/invalid `v-for`. In that
             // case SSR only rendered the parent slot range, so this `v-for` has no
             // own `<!--]-->` to reuse. If `hydrationStart` is not the parent slot
-            // end anchor, use `hydrationStart.nextSibling` as the preserve anchor
+            // end anchor, use `hydrationStart.nextSibling` as the insertion point
             // so the runtime `<!--for-->` lands immediately after that local SSR
             // range. Otherwise insert it before the parent slot end anchor.
             if (slotFallbackRange && !isValidBlock(newBlocks)) {
-              patchCurrentHydrationBoundary({ close: currentSlotEndAnchor })
               const anchor =
                 // The invalid list still consumed local SSR item ranges.
                 currentHydrationNode !== hydrationStart
@@ -185,10 +169,6 @@ export const createFor = (
               parentAnchor = markHydrationAnchor(
                 __DEV__ ? createComment('for') : createTextNode(),
               )
-              patchCurrentHydrationBoundary({
-                preserve: parentAnchor,
-                cleanupOnPop: false,
-              })
               pendingHydrationAnchor = true
               if (
                 currentHydrationNode === hydrationStart ||
@@ -201,13 +181,8 @@ export const createFor = (
               )
             } else {
               const close = locateHydrationBoundaryClose(currentHydrationNode!)
-
               parentAnchor = markHydrationAnchor(close)
-              patchCurrentHydrationBoundary({
-                close: parentAnchor,
-                preserve: parentAnchor,
-                cleanupOnPop: true,
-              })
+              exitHydrationBoundary = enterHydrationBoundary(parentAnchor)
               if (__DEV__ && !isComment(parentAnchor, ']')) {
                 throw new Error(
                   `v-for fragment anchor node was not found. this is likely a Vue internal bug.`,
@@ -223,7 +198,7 @@ export const createFor = (
             }
           }
         } finally {
-          restoreBoundary()
+          exitHydrationBoundary && exitHydrationBoundary()
         }
       } else {
         for (let i = 0; i < newLength; i++) {

+ 6 - 8
packages/runtime-vapor/src/component.ts

@@ -87,13 +87,13 @@ import {
   adoptTemplate,
   advanceHydrationNode,
   currentHydrationNode,
+  enterHydrationBoundary,
   isComment,
   isHydrating,
   locateEndAnchor,
   locateHydrationNode,
   locateNextNode,
   markHydrationAnchor,
-  pushHydrationBoundary,
   setCurrentHydrationNode,
 } from './dom/hydration'
 import { createComment, createElement, createTextNode } from './dom/node'
@@ -249,16 +249,14 @@ export function createComponent(
   const _insertionAnchor = insertionAnchor
   const _isLastInsertion = isLastInsertion
   let hydrationClose: Node | null = null
-  let restoreBoundary: (() => void) | undefined
+  let exitHydrationBoundary: (() => void) | undefined
   if (isHydrating) {
     locateHydrationNode()
     if (component.__multiRoot && isComment(currentHydrationNode!, '[')) {
       hydrationClose = locateEndAnchor(currentHydrationNode!)
-      restoreBoundary = pushHydrationBoundary({
-        close: hydrationClose,
-        preserve: hydrationClose && markHydrationAnchor(hydrationClose),
-        cleanupOnPop: true,
-      })
+      exitHydrationBoundary = enterHydrationBoundary(
+        hydrationClose && markHydrationAnchor(hydrationClose),
+      )
       setCurrentHydrationNode(currentHydrationNode!.nextSibling)
     }
   } else {
@@ -412,7 +410,7 @@ export function createComponent(
     return instance
   } finally {
     if (isHydrating) {
-      restoreBoundary && restoreBoundary()
+      exitHydrationBoundary && exitHydrationBoundary()
       if (hydrationClose && currentHydrationNode === hydrationClose) {
         advanceHydrationNode(hydrationClose)
       }

+ 45 - 68
packages/runtime-vapor/src/components/Teleport.ts

@@ -40,8 +40,6 @@ import {
   isHydrating,
   logMismatchError,
   markHydrationAnchor,
-  patchCurrentHydrationBoundary,
-  pushHydrationBoundary,
   runWithoutHydration,
   setCurrentHydrationNode,
 } from '../dom/hydration'
@@ -442,79 +440,58 @@ export class TeleportFragment extends VaporFragment {
 
   hydrate = (): void => {
     if (!isHydrating) return
-    const restoreBoundary = pushHydrationBoundary({})
-    try {
-      const target = (this.target = resolveTeleportTarget(
-        this.resolvedProps!,
-        querySelector,
-      ))
-      const disabled = isTeleportDisabled(this.resolvedProps!)
-      this.placeholder = currentHydrationNode!
-      if (target) {
-        const targetNode =
-          (target as TeleportTargetElement)._lpa || target.firstChild
-        if (disabled) {
-          this.hydrateDisabledTeleport(
-            target as TeleportTargetElement,
-            targetNode,
-          )
-          patchCurrentHydrationBoundary({
-            close: this.anchor,
-            preserve: this.anchor,
-          })
-        } else {
-          this.anchor = markHydrationAnchor(
-            locateTeleportEndAnchor(currentHydrationNode!.nextSibling!)!,
-          )
-          patchCurrentHydrationBoundary({
-            close: this.anchor,
-            preserve: this.anchor,
-          })
-          this.mountContainer = target
-          this.hydrateTargetAnchors(target as TeleportTargetElement, targetNode)
-          this.mountAnchor = this.targetAnchor
-
-          if (targetNode) {
-            setCurrentHydrationNode(targetNode.nextSibling)
-          }
-
-          // if the HTML corresponding to Teleport is not embedded in the
-          // correct position on the final page during SSR. the targetAnchor will
-          // always be null, we need to manually add targetAnchor to ensure
-          // Teleport it can properly unmount or move
-          if (!this.targetAnchor) {
-            this.mountChildren(target)
-          } else {
-            this.initChildren()
-          }
-        }
-      } else if (disabled) {
-        // pass null as targetNode since there is no target
-        this.hydrateDisabledTeleport(null, null)
-        patchCurrentHydrationBoundary({
-          close: this.anchor,
-          preserve: this.anchor,
-        })
+    const target = (this.target = resolveTeleportTarget(
+      this.resolvedProps!,
+      querySelector,
+    ))
+    const disabled = isTeleportDisabled(this.resolvedProps!)
+    this.placeholder = currentHydrationNode!
+    if (target) {
+      const targetNode =
+        (target as TeleportTargetElement)._lpa || target.firstChild
+      if (disabled) {
+        this.hydrateDisabledTeleport(
+          target as TeleportTargetElement,
+          targetNode,
+        )
       } else {
-        // Align with VDOM Teleport hydration: keep main-view markers only and
-        // avoid mounting children inline or eagerly initializing them when the
-        // target is missing.
         this.anchor = markHydrationAnchor(
           locateTeleportEndAnchor(currentHydrationNode!.nextSibling!)!,
         )
-        patchCurrentHydrationBoundary({
-          close: this.anchor,
-          preserve: this.anchor,
-        })
-      }
+        this.mountContainer = target
+        this.hydrateTargetAnchors(target as TeleportTargetElement, targetNode)
+        this.mountAnchor = this.targetAnchor
+
+        if (targetNode) {
+          setCurrentHydrationNode(targetNode.nextSibling)
+        }
 
-      if (target || disabled) {
-        updateCssVars(this)
+        // if the HTML corresponding to Teleport is not embedded in the
+        // correct position on the final page during SSR. the targetAnchor will
+        // always be null, we need to manually add targetAnchor to ensure
+        // Teleport it can properly unmount or move
+        if (!this.targetAnchor) {
+          this.mountChildren(target)
+        } else {
+          this.initChildren()
+        }
       }
-      advanceHydrationNode(this.anchor!)
-    } finally {
-      restoreBoundary()
+    } else if (disabled) {
+      // pass null as targetNode since there is no target
+      this.hydrateDisabledTeleport(null, null)
+    } else {
+      // Align with VDOM Teleport hydration: keep main-view markers only and
+      // avoid mounting children inline or eagerly initializing them when the
+      // target is missing.
+      this.anchor = markHydrationAnchor(
+        locateTeleportEndAnchor(currentHydrationNode!.nextSibling!)!,
+      )
+    }
+
+    if (target || disabled) {
+      updateCssVars(this)
     }
+    advanceHydrationNode(this.anchor!)
   }
 }
 

+ 61 - 82
packages/runtime-vapor/src/dom/hydration.ts

@@ -28,76 +28,6 @@ export function setIsHydratingEnabled(value: boolean): void {
 
 export let currentHydrationNode: Node | null = null
 
-export interface HydrationBoundary {
-  // Structural close marker the current owner must not cross during cleanup.
-  close?: Node | null
-  // Marker mismatch recovery must insert before instead of replacing.
-  preserve?: Node | null
-  // Whether restore should trim unclaimed SSR nodes up to `close`.
-  cleanupOnPop?: boolean
-}
-
-export let currentHydrationBoundary: HydrationBoundary | null = null
-
-function finalizeHydrationBoundary(boundary: HydrationBoundary): void {
-  const close = boundary.close
-  let node = currentHydrationNode
-
-  if (!close || !node || node === close || node === boundary.preserve) {
-    return
-  }
-
-  // This boundary only owns cleanup while the current cursor is still inside
-  // its SSR range. If nested hydration has already advanced past `close`, stop
-  // here so we don't delete sibling or parent-owned SSR nodes by mistake.
-  let cur: Node | null = node
-  while (cur && cur !== close) {
-    cur = locateNextNode(cur)
-  }
-  if (!cur) return
-
-  warnHydrationChildrenMismatch((close as Node).parentElement)
-
-  while (node && node !== close) {
-    const next = locateNextNode(node)
-    removeHydrationNode(node, close)
-    node = next!
-  }
-
-  setCurrentHydrationNode(close)
-}
-
-function warnHydrationChildrenMismatch(container: Element | null): void {
-  if (container && !isMismatchAllowed(container, MismatchTypes.CHILDREN)) {
-    ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
-      warn(
-        `Hydration children mismatch on`,
-        container,
-        `\nServer rendered element contains more child nodes than client nodes.`,
-      )
-    logMismatchError()
-  }
-}
-
-export function pushHydrationBoundary(boundary: HydrationBoundary): () => void {
-  const prev = currentHydrationBoundary
-  currentHydrationBoundary = boundary
-  return () => {
-    if (boundary.cleanupOnPop) {
-      finalizeHydrationBoundary(boundary)
-    }
-    currentHydrationBoundary = prev
-  }
-}
-
-export function patchCurrentHydrationBoundary(
-  patch: Partial<HydrationBoundary>,
-): void {
-  if (currentHydrationBoundary) {
-    Object.assign(currentHydrationBoundary, patch)
-  }
-}
-
 export let isHydrating = false
 function setIsHydrating(value: boolean) {
   if (!isHydratingEnabled && !isVdomHydrating) return false
@@ -137,7 +67,6 @@ function performHydration<T>(
   }
   const prev = setIsHydrating(true)
   const prevHydrationNode = currentHydrationNode
-  const prevHydrationBoundary = currentHydrationBoundary
   currentHydrationNode = null
   try {
     setup()
@@ -145,7 +74,6 @@ function performHydration<T>(
   } finally {
     cleanup()
     currentHydrationNode = prevHydrationNode
-    currentHydrationBoundary = prevHydrationBoundary
     setIsHydrating(prev)
   }
 }
@@ -153,7 +81,6 @@ function performHydration<T>(
 export function withHydration(container: ParentNode, fn: () => void): void {
   const setup = () => {
     setInsertionState(container)
-    currentHydrationBoundary = {}
   }
   const cleanup = () => resetInsertionState()
   return performHydration(fn, setup, cleanup)
@@ -162,7 +89,6 @@ export function withHydration(container: ParentNode, fn: () => void): void {
 export function hydrateNode(node: Node, fn: () => void): void {
   const setup = () => {
     currentHydrationNode = node
-    currentHydrationBoundary = {}
   }
   const cleanup = () => {}
   return performHydration(fn, setup, cleanup)
@@ -176,13 +102,10 @@ export function enterHydration(node: Node): () => void {
 
   const prev = setIsHydrating(true)
   const prevHydrationNode = currentHydrationNode
-  const prevHydrationBoundary = currentHydrationBoundary
   currentHydrationNode = node
-  currentHydrationBoundary = {}
 
   return () => {
     currentHydrationNode = prevHydrationNode
-    currentHydrationBoundary = prevHydrationBoundary
     setIsHydrating(prev)
     if (!prevHydrationEnabled) {
       setIsHydratingEnabled(false)
@@ -194,8 +117,8 @@ export let adoptTemplate: (node: Node, template: string) => Node | null
 export let locateHydrationNode: (consumeFragmentStart?: boolean) => void
 
 type Anchor = Node & {
-  // Runtime-created or reused preserve anchor that must stay in place during
-  // hydration mismatch recovery.
+  // Runtime-created or reused hydration anchor that mismatch recovery and
+  // boundary cleanup must keep in place.
   $vha?: 1
 
   // cached matching fragment end to avoid repeated traversal on nested
@@ -319,8 +242,7 @@ export function locateEndAnchor(
   return null
 }
 
-// Find the SSR close marker for the current owner and cache it on the active
-// boundary so restore-time cleanup can reuse the same structural limit.
+// Find the SSR close marker for the current owner.
 export function locateHydrationBoundaryClose(
   node: Node,
   closeHint: Node | null = null,
@@ -342,7 +264,6 @@ export function locateHydrationBoundaryClose(
     return node
   }
 
-  patchCurrentHydrationBoundary({ close })
   return close
 }
 
@@ -489,3 +410,61 @@ function resolveHydrationTarget(node: Node): Node {
     return node
   }
 }
+
+function finalizeHydrationBoundary(close: Node | null): void {
+  let node = currentHydrationNode
+
+  // Once the hydration cursor has already reached `close`, this scope has no
+  // unclaimed SSR nodes left to trim. Single-root paths commonly end up here,
+  // so there is no children-count mismatch to report for this boundary.
+  if (!close || !node || node === close) {
+    return
+  }
+
+  // This boundary only owns cleanup while the current cursor is still inside
+  // its SSR range. If nested hydration has already advanced past `close`, stop
+  // here so we don't delete sibling or parent-owned SSR nodes by mistake.
+  let cur: Node | null = node
+  let hasRemovableNode = false
+  while (cur && cur !== close) {
+    if (!isHydrationAnchor(cur)) {
+      hasRemovableNode = true
+    }
+    cur = locateNextNode(cur)
+  }
+  if (!cur) return
+  if (!hasRemovableNode) {
+    setCurrentHydrationNode(close)
+    return
+  }
+
+  warnHydrationChildrenMismatch((close as Node).parentElement)
+
+  while (node && node !== close) {
+    const next = locateNextNode(node)
+    if (!isHydrationAnchor(node)) {
+      removeHydrationNode(node, close)
+    }
+    node = next!
+  }
+
+  setCurrentHydrationNode(close)
+}
+
+function warnHydrationChildrenMismatch(container: Element | null): void {
+  if (container && !isMismatchAllowed(container, MismatchTypes.CHILDREN)) {
+    ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      warn(
+        `Hydration children mismatch on`,
+        container,
+        `\nServer rendered element contains more child nodes than client nodes.`,
+      )
+    logMismatchError()
+  }
+}
+
+export function enterHydrationBoundary(close: Node | null): () => void {
+  return () => {
+    finalizeHydrationBoundary(close)
+  }
+}

+ 9 - 31
packages/runtime-vapor/src/fragment.ts

@@ -33,6 +33,7 @@ import {
   advanceHydrationNode,
   cleanupHydrationTail,
   currentHydrationNode,
+  enterHydrationBoundary,
   isComment,
   isHydrating,
   locateEndAnchor,
@@ -40,8 +41,6 @@ import {
   locateHydrationNode,
   locateNextNode,
   markHydrationAnchor,
-  patchCurrentHydrationBoundary,
-  pushHydrationBoundary,
   setCurrentHydrationNode,
 } from './dom/hydration'
 import { isArray } from '@vue/shared'
@@ -374,7 +373,7 @@ export class DynamicFragment extends VaporFragment {
     if (this.isAnchorPending) return
 
     let advanceAfterRestore: Node | null = null
-    const restoreBoundary = pushHydrationBoundary({})
+    let exitHydrationBoundary: (() => void) | undefined
 
     try {
       // reuse `<!---->` as anchor
@@ -382,10 +381,6 @@ export class DynamicFragment extends VaporFragment {
       if (isEmpty) {
         if (isComment(currentHydrationNode!, '')) {
           this.anchor = markHydrationAnchor(currentHydrationNode!)
-          patchCurrentHydrationBoundary({
-            close: currentHydrationNode,
-            preserve: this.anchor,
-          })
           advanceHydrationNode(currentHydrationNode)
           return
         }
@@ -405,12 +400,8 @@ export class DynamicFragment extends VaporFragment {
         this.anchor = markHydrationAnchor(this.nodes)
         this.nodes = []
         const needsCleanup = currentHydrationNode !== this.anchor
-        patchCurrentHydrationBoundary({
-          close: this.anchor,
-          preserve: this.anchor,
-          cleanupOnPop: needsCleanup,
-        })
         if (needsCleanup) {
+          exitHydrationBoundary = enterHydrationBoundary(this.anchor)
           advanceAfterRestore = this.anchor
         } else {
           advanceHydrationNode(this.anchor)
@@ -435,10 +426,7 @@ export class DynamicFragment extends VaporFragment {
         if (parentNode) {
           this.nodes = []
           if (nextNode) {
-            patchCurrentHydrationBoundary({
-              close: nextNode,
-              cleanupOnPop: true,
-            })
+            exitHydrationBoundary = enterHydrationBoundary(nextNode)
           } else {
             cleanupHydrationTail(currentHydrationNode)
             setCurrentHydrationNode(null)
@@ -464,7 +452,6 @@ export class DynamicFragment extends VaporFragment {
           (!isValidBlock(this.nodes) || currentEmptyFragment === this)
         ) {
           const endAnchor = currentSlotEndAnchor
-          patchCurrentHydrationBoundary({ close: endAnchor })
           this.isAnchorPending = true
           queuePostFlushCb(() =>
             endAnchor.parentNode!.insertBefore(
@@ -496,11 +483,7 @@ export class DynamicFragment extends VaporFragment {
         )
         if (isComment(anchor!, ']')) {
           this.anchor = markHydrationAnchor(anchor)
-          patchCurrentHydrationBoundary({
-            close: anchor,
-            preserve: this.anchor,
-            cleanupOnPop: true,
-          })
+          exitHydrationBoundary = enterHydrationBoundary(anchor)
           advanceHydrationNode(anchor)
           return
         } else if (__DEV__) {
@@ -536,7 +519,7 @@ export class DynamicFragment extends VaporFragment {
         )
       })
     } finally {
-      restoreBoundary()
+      exitHydrationBoundary && exitHydrationBoundary()
       if (advanceAfterRestore && currentHydrationNode === advanceAfterRestore) {
         advanceHydrationNode(advanceAfterRestore)
       }
@@ -595,7 +578,7 @@ export class SlotFragment extends DynamicFragment {
   ): void {
     let prevEndAnchor: Node | null = null
     let pushedEndAnchor = false
-    let restoreBoundary: (() => void) | undefined
+    let exitHydrationBoundary: (() => void) | undefined
     if (isHydrating) {
       locateHydrationNode()
       if (isComment(currentHydrationNode!, '[')) {
@@ -603,12 +586,7 @@ export class SlotFragment extends DynamicFragment {
         setCurrentHydrationNode(currentHydrationNode.nextSibling)
         prevEndAnchor = setCurrentSlotEndAnchor(endAnchor)
         pushedEndAnchor = true
-        restoreBoundary = pushHydrationBoundary({
-          close: endAnchor,
-          cleanupOnPop: true,
-        })
-      } else {
-        restoreBoundary = pushHydrationBoundary({ cleanupOnPop: true })
+        exitHydrationBoundary = enterHydrationBoundary(endAnchor)
       }
     }
 
@@ -651,7 +629,7 @@ export class SlotFragment extends DynamicFragment {
       if (isHydrating && pushedEndAnchor) {
         setCurrentSlotEndAnchor(prevEndAnchor)
       }
-      restoreBoundary && restoreBoundary()
+      exitHydrationBoundary && exitHydrationBoundary()
     }
   }
 }