]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: hydration vdom interop
authordaiwei <daiwei521@126.com>
Wed, 6 Aug 2025 09:38:55 +0000 (17:38 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 7 Aug 2025 01:49:02 +0000 (09:49 +0800)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/hydration.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/components/Teleport.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/vdomInterop.ts

index 43c96e5003ad45223904bb94cf93fbdbd43110ff..0f379b4138746afdbeb2c57e80cc7d9676241efa 100644 (file)
@@ -191,8 +191,8 @@ export interface VaporInteropInterface {
     component: ComponentInternalInstance,
     transition: TransitionHooks,
   ): void
-  hydrate(node: Node, fn: () => void): void
-  hydrateSlot(vnode: VNode, container: any): void
+  hydrate(node: Node, fn: () => void): Node
+  hydrateSlot(vnode: VNode, node: any): Node
 
   vdomMount: (
     component: ConcreteComponent,
index 5db9a3bdd5218bfacf02223643853f47ea081ec8..f7f15d2f3b0c6ce0a7f371cece3ba72af5b542a6 100644 (file)
@@ -279,9 +279,9 @@ export function createHydrationFunctions(
         }
         break
       case VaporSlot:
-        getVaporInterface(parentComponent, vnode).hydrateSlot(
+        nextNode = getVaporInterface(parentComponent, vnode).hydrateSlot(
           vnode,
-          parentNode(node)!,
+          node,
         )
         break
       default:
@@ -327,7 +327,7 @@ export function createHydrationFunctions(
           // hydrate vapor component
           if ((vnode.type as ConcreteComponent).__vapor) {
             const vaporInterface = getVaporInterface(parentComponent, vnode)
-            vaporInterface.hydrate(node, () => {
+            nextNode = vaporInterface.hydrate(node, () => {
               vaporInterface.mount(vnode, container, null, parentComponent)
             })
           } else {
index 1bf7767352bc59d9b12e7e1bf5c3e42bc8315767..659f8487a0f0f760a4a1c33666aaa6efba38aeca 100644 (file)
@@ -49,11 +49,13 @@ export function createDynamicComponent(
     )
   })
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
-  }
-  if (isHydrating && _insertionAnchor !== undefined) {
-    advanceHydrationNode(_insertionParent!)
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
+    if (_insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
+
   return frag
 }
index 249d03d71738ff3a586e84450b1ec1a0debd8789..09a3d2fc518994452715bbf0fde46c163c222b05 100644 (file)
@@ -471,10 +471,9 @@ export const createFor = (
     renderEffect(renderList)
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
-  }
-  if (isHydrating) {
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
     advanceHydrationNode(
       _insertionAnchor !== undefined ? _insertionParent! : parentAnchor!,
     )
index 4ed055554d577cba03808902b11a748a6cd8c372..ac9f1ffd9a27e620b2a3d981e8e92824a510a91e 100644 (file)
@@ -74,15 +74,15 @@ export function createIf(
     isHydrating && ifStack.pop()
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
-  }
-
-  // if _insertionAnchor is defined, insertionParent contains a static node
-  // that should be skipped during hydration.
-  // Advance to the next sibling node to bypass this static node.
-  if (isHydrating && _insertionAnchor !== undefined) {
-    advanceHydrationNode(_insertionParent!)
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
+    // if _insertionAnchor is defined, insertionParent contains a static node
+    // that should be skipped during hydration.
+    // Advance to the next sibling node to bypass this static node.
+    if (_insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return frag
index f9bac4347f4c9bcad40c3e524fccf28aa9b8d637..a6bc4869aaba6a2e9c7d7a27cebc6615784c50e2 100644 (file)
@@ -179,27 +179,28 @@ export function createComponent(
       scopeId,
     )
 
-    // `frag.insert` handles both hydration and mounting
-    if (_insertionParent) {
-      insert(frag, _insertionParent, _insertionAnchor)
-    }
-    if (isHydrating && _insertionAnchor !== undefined) {
-      advanceHydrationNode(_insertionParent!)
+    if (!isHydrating) {
+      if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+    } else {
+      frag.hydrate()
+      if (_insertionAnchor !== undefined) {
+        advanceHydrationNode(_insertionParent!)
+      }
     }
+
     return frag
   }
 
   // teleport
   if (isVaporTeleport(component)) {
     const frag = component.process(rawProps!, rawSlots!)
-    if (!isHydrating && _insertionParent) {
-      insert(frag, _insertionParent, _insertionAnchor)
+    if (!isHydrating) {
+      if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
     } else {
       frag.hydrate()
-    }
-
-    if (isHydrating && _insertionAnchor !== undefined) {
-      advanceHydrationNode(_insertionParent!)
+      if (_insertionAnchor !== undefined) {
+        advanceHydrationNode(_insertionParent!)
+      }
     }
 
     return frag as any
@@ -316,8 +317,14 @@ export function createComponent(
 
   if (scopeId) setScopeId(instance.block, scopeId)
 
-  if (!isHydrating && _insertionParent) {
-    mountComponent(instance, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) {
+      mountComponent(instance, _insertionParent, _insertionAnchor)
+    }
+  } else {
+    if (_insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
   return instance
 }
@@ -591,12 +598,12 @@ export function createComponentWithFallback(
     }
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(el, _insertionParent, _insertionAnchor)
-  }
-
-  if (isHydrating && _insertionAnchor !== undefined) {
-    advanceHydrationNode(_insertionParent!)
+  if (!isHydrating) {
+    if (_insertionParent) insert(el, _insertionParent, _insertionAnchor)
+  } else {
+    if (_insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return el
index 3b4adeed9f1a9cfec9eecf05635f86abb5b831ea..1e8b30128e1e49058dedc8b38d6550c562847887 100644 (file)
@@ -16,8 +16,12 @@ import {
   insertionParent,
   resetInsertionState,
 } from './insertionState'
-import { advanceHydrationNode, isHydrating } from './dom/hydration'
-import { DynamicFragment } from './fragment'
+import {
+  advanceHydrationNode,
+  isHydrating,
+  locateHydrationNode,
+} from './dom/hydration'
+import { DynamicFragment, type VaporFragment } from './fragment'
 
 export type RawSlots = Record<string, VaporSlot> & {
   $?: DynamicSlotSource[]
@@ -127,6 +131,7 @@ export function createSlot(
 
   let fragment: DynamicFragment
   if (isRef(rawSlots._)) {
+    if (isHydrating) locateHydrationNode()
     fragment = instance.appContext.vapor!.vdomSlot(
       rawSlots._,
       name,
@@ -166,17 +171,15 @@ export function createSlot(
     if (scopeId) setScopeId(fragment, `${scopeId}-s`)
   }
 
-  if (
-    _insertionParent &&
-    (!isHydrating ||
-      // for vdom interop fragment, `fragment.insert` handles both hydration and mounting
-      fragment.insert)
-  ) {
-    insert(fragment, _insertionParent, _insertionAnchor)
-  }
-
-  if (isHydrating && _insertionAnchor !== undefined) {
-    advanceHydrationNode(_insertionParent!)
+  if (!isHydrating) {
+    if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor)
+  } else {
+    if (fragment.insert) {
+      ;(fragment as VaporFragment).hydrate!()
+    }
+    if (_insertionAnchor !== undefined) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return fragment
index dc4dab68cdb5348efc74fb8e61acade2e852f934..d80399373ed817c154a45510b38b229a54514c20 100644 (file)
@@ -209,7 +209,7 @@ export class TeleportFragment extends VaporFragment {
     this.mountAnchor = undefined
   }
 
-  hydrate(): void {
+  hydrate = (): void => {
     // TODO
   }
 }
index 53d83adf3de695fc1e5d26ed55ffb21e5466aba8..9cf99e38e0c658715f30095bf52e0a4d7c518c14 100644 (file)
@@ -36,6 +36,7 @@ export class VaporFragment<T extends Block = Block>
     anchor: Node | null,
     transitionHooks?: TransitionHooks,
   ) => void
+  hydrate?: (...args: any[]) => any
   remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
   fallback?: BlockFn
 
@@ -155,7 +156,7 @@ export class DynamicFragment extends VaporFragment {
     }
   }
 
-  hydrate(label: string, isEmpty: boolean = false): void {
+  hydrate = (label: string, isEmpty: boolean = false): void => {
     // for `v-if="false"`, the node will be an empty comment, use it as the anchor.
     // otherwise, find next sibling vapor fragment anchor
     if (label === 'if' && isEmpty) {
index d16c73127d2475d7610db69f3507dbb807cbd332..af8bef74062412f107deefdc2e93c7493218da7a 100644 (file)
@@ -2,6 +2,7 @@ import {
   type App,
   type ComponentInternalInstance,
   type ConcreteComponent,
+  Fragment,
   type HydrationRenderer,
   MoveType,
   type Plugin,
@@ -20,6 +21,7 @@ import {
   ensureRenderer,
   ensureVaporSlotFallback,
   isEmitListener,
+  isRef,
   isVNode,
   onScopeDispose,
   renderSlot,
@@ -34,6 +36,7 @@ import {
   type VaporComponent,
   VaporComponentInstance,
   createComponent,
+  isVaporComponent,
   mountComponent,
   unmountComponent,
 } from './component'
@@ -54,9 +57,11 @@ import { setTransitionHooks as setVaporTransitionHooks } from './components/Tran
 import {
   advanceHydrationNode,
   currentHydrationNode,
+  isComment,
   isHydrating,
   locateHydrationNode,
   locateVaporFragmentAnchor,
+  setCurrentHydrationNode,
   hydrateNode as vaporHydrateNode,
 } from './dom/hydration'
 import { VaporFragment, isFragment, setFragmentFallback } from './fragment'
@@ -99,6 +104,10 @@ const vaporInteropImpl: Omit<
       {
         _: slotsRef, // pass the slots ref
       } as any as RawSlots,
+      undefined,
+      undefined,
+      undefined,
+      (parentComponent ? parentComponent.appContext : vnode.appContext) as any,
     ))
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef
@@ -169,16 +178,21 @@ const vaporInteropImpl: Omit<
     setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks)
   },
 
-  hydrate: vaporHydrateNode,
-  hydrateSlot(vnode, container) {
+  hydrate(node, fn) {
+    vaporHydrateNode(node, fn)
+    return __next(node)
+  },
+  hydrateSlot(vnode, node) {
     const { slot } = vnode.vs!
     const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
-    const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
-    vaporHydrateNode(slotBlock, () => {
-      const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')!
-      vnode.el = vnode.anchor = anchor
-      insert((vnode.vb = slotBlock), container, anchor)
+    vaporHydrateNode(node, () => {
+      vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+      vnode.el = vnode.anchor = locateVaporFragmentAnchor(
+        currentHydrationNode!,
+        'slot',
+      )
     })
+    return __next(node)
   },
 }
 
@@ -256,25 +270,29 @@ function createVDOMComponent(
 
   vnode.scopeId = parentInstance && parentInstance.type.__scopeId!
 
+  frag.hydrate = () => {
+    hydrateVNode(vnode, parentInstance as any)
+    onScopeDispose(unmount, true)
+    isMounted = true
+    frag.nodes = [vnode.el as Node]
+  }
+
   frag.insert = (parentNode, anchor, transition) => {
+    if (isHydrating) return
+
     const prev = currentInstance
     simpleSetCurrentInstance(parentInstance)
-    if (!isMounted || isHydrating) {
+    if (!isMounted) {
       if (transition) setVNodeTransitionHooks(vnode, transition)
-
-      if (isHydrating) {
-        hydrateVNode(vnode, parentInstance as any)
-      } else {
-        internals.mt(
-          vnode,
-          parentNode,
-          anchor,
-          parentInstance as any,
-          null,
-          undefined,
-          false,
-        )
-      }
+      internals.mt(
+        vnode,
+        parentNode,
+        anchor,
+        parentInstance as any,
+        null,
+        undefined,
+        false,
+      )
       onScopeDispose(unmount, true)
       isMounted = true
     } else {
@@ -317,76 +335,14 @@ function renderVDOMSlot(
 
   frag.fallback = fallback
   frag.insert = (parentNode, anchor) => {
-    if (!isMounted) {
-      renderEffect(() => {
-        let vnode: VNode | undefined
-        let isValidSlot = false
-        // only render slot if rawSlots is defined and slot nodes are not empty
-        // otherwise, render fallback
-        if (slotsRef.value) {
-          vnode = renderSlot(
-            slotsRef.value,
-            isFunction(name) ? name() : name,
-            props,
-          )
+    if (isHydrating) return
 
-          let children = vnode.children as any[]
-          // handle forwarded vapor slot without its own fallback
-          // use the fallback provided by the slot outlet
-          ensureVaporSlotFallback(children, fallback as any)
-          isValidSlot = children.length > 0
-        }
-        if (isValidSlot) {
-          if (isHydrating) {
-            hydrateVNode(vnode!, parentComponent as any)
-          } else {
-            if (fallbackNodes) {
-              remove(fallbackNodes, parentNode)
-              fallbackNodes = undefined
-            }
-            internals.p(
-              oldVNode,
-              vnode!,
-              parentNode,
-              anchor,
-              parentComponent as any,
-              null,
-              undefined,
-              null,
-              false,
-            )
-          }
-          oldVNode = vnode!
-        } else {
-          // for forwarded slot without its own fallback, use the fallback
-          // provided by the slot outlet.
-          // re-fetch `frag.fallback` as it may have been updated at `createSlot`
-          fallback = frag.fallback
-          if (fallback && !fallbackNodes) {
-            // mount fallback
-            if (oldVNode) {
-              internals.um(oldVNode, parentComponent as any, null, true)
-            }
-            insert(
-              (fallbackNodes = fallback(internals, parentComponent)),
-              parentNode,
-              anchor,
-            )
-          } else if (isHydrating) {
-            // update hydration node to the next sibling of the slot anchor
-            const nextNode = locateVaporFragmentAnchor(
-              currentHydrationNode!,
-              'slot',
-            )
-            if (nextNode) advanceHydrationNode(nextNode)
-          }
-          oldVNode = null
-        }
-      })
+    if (!isMounted) {
+      render(parentNode, anchor)
       isMounted = true
     } else {
       // move
-      if (oldVNode && !isHydrating) {
+      if (oldVNode) {
         internals.m(
           oldVNode,
           parentNode,
@@ -406,6 +362,72 @@ function renderVDOMSlot(
     }
   }
 
+  const render = (parentNode?: ParentNode, anchor?: Node | null) => {
+    renderEffect(() => {
+      let vnode: VNode | undefined
+      let isValidSlot = false
+      if (slotsRef.value) {
+        vnode = renderSlot(
+          slotsRef.value,
+          isFunction(name) ? name() : name,
+          props,
+        )
+
+        let children = vnode.children as any[]
+        // handle forwarded vapor slot without its own fallback
+        // use the fallback provided by the slot outlet
+        ensureVaporSlotFallback(children, fallback as any)
+        isValidSlot = children.length > 0
+      }
+      if (isValidSlot) {
+        if (isHydrating) {
+          hydrateVNode(vnode!, parentComponent as any)
+        } else {
+          if (fallbackNodes) {
+            remove(fallbackNodes, parentNode)
+            fallbackNodes = undefined
+          }
+          internals.p(
+            oldVNode,
+            vnode!,
+            parentNode!,
+            anchor,
+            parentComponent as any,
+            null,
+            undefined,
+            null,
+            false,
+          )
+        }
+        oldVNode = vnode!
+      } else {
+        // for forwarded slot without its own fallback, use the fallback
+        // provided by the slot outlet.
+        // re-fetch `frag.fallback` as it may have been updated at `createSlot`
+        fallback = frag.fallback
+        if (fallback && !fallbackNodes) {
+          fallbackNodes = fallback(internals, parentComponent)
+          if (isHydrating) {
+            // hydrate fallback
+            hydrateVNode(fallbackNodes as any, parentComponent as any)
+          } else {
+            // mount fallback
+            if (oldVNode) {
+              internals.um(oldVNode, parentComponent as any, null, true)
+            }
+            insert(fallbackNodes, parentNode!, anchor)
+          }
+        }
+        oldVNode = null
+      }
+    })
+  }
+
+  frag.hydrate = () => {
+    render()
+    isMounted = true
+  }
+
   return frag
 }
 
@@ -460,9 +482,24 @@ function hydrateVNode(
   vnode: VNode,
   parentComponent: ComponentInternalInstance | null,
 ) {
-  // keep fragment start anchor, hydrateNode uses it to
-  // determine if node is a fragmentStart
   locateHydrationNode()
+
+  // skip fragment start anchor
+  let node = currentHydrationNode!
+  while (
+    isComment(node, '[') &&
+    // vnode is not a fragment
+    vnode.type !== Fragment &&
+    // not inside vdom slot
+    !(
+      isVaporComponent(parentComponent) &&
+      isRef((parentComponent as VaporComponentInstance).rawSlots._)
+    )
+  ) {
+    node = node.nextSibling!
+  }
+  if (currentHydrationNode !== node) setCurrentHydrationNode(node)
+
   if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
   const nextNode = vdomHydrateNode(
     currentHydrationNode!,