]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: create anchor for DynamicFragment when necessary
authordaiwei <daiwei521@126.com>
Thu, 7 Aug 2025 07:21:37 +0000 (15:21 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 7 Aug 2025 07:21:37 +0000 (15:21 +0800)
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/fragment.ts
packages/shared/src/domAnchors.ts

index 6ec5f09e66d79bb12f1f4b0e7c30ff00ab06eb6e..7b471ab7ae640d2a78af71e0d3fb620e4ea09bd6 100644 (file)
@@ -1195,11 +1195,15 @@ describe('Vapor Mode hydration', () => {
 
       data.value = 'b'
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
+      expect(container.innerHTML).toBe(
+        `<div>bar</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
 
       data.value = 'c'
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>baz</div><!--${anchorLabel}-->`)
+      expect(container.innerHTML).toBe(
+        `<div>baz</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
 
       data.value = 'a'
       await nextTick()
@@ -1390,13 +1394,13 @@ describe('Vapor Mode hydration', () => {
       data.value = 'b'
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<span>b child2</span><!--${anchorLabel}-->`,
+        `<span>b child2</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
       )
 
       data.value = 'c'
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<span>c child3</span><!--${anchorLabel}-->`,
+        `<span>c child3</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
       )
 
       data.value = 'a'
@@ -2762,13 +2766,13 @@ describe('Vapor Mode hydration', () => {
       )
 
       expect(container.innerHTML).toBe(
-        `<div><!--[--><!--]--><!--slot--><div>foo</div></div>`,
+        `<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>foo</div></div>`,
       )
 
       data.foo = 'bar'
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<div><!--[--><!--]--><!--slot--><div>bar</div></div>`,
+        `<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>bar</div></div>`,
       )
     })
   })
index ac9f1ffd9a27e620b2a3d981e8e92824a510a91e..cec91af0a9ecf39957810ff5ccc0839e44661054 100644 (file)
@@ -1,4 +1,4 @@
-import { IF_ANCHOR_LABEL } from '@vue/shared'
+import { ELSE_IF_ANCHOR_LABEL, IF_ANCHOR_LABEL } from '@vue/shared'
 import { type Block, type BlockFn, insert } from './block'
 import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import {
@@ -56,8 +56,10 @@ export function createIf(
     frag = condition() ? b1() : b2 ? b2() : []
   } else {
     frag =
-      (isHydrating || __DEV__) && !elseIf
-        ? new DynamicFragment(IF_ANCHOR_LABEL)
+      isHydrating || __DEV__
+        ? new DynamicFragment(
+            elseIf && isHydrating ? ELSE_IF_ANCHOR_LABEL : IF_ANCHOR_LABEL,
+          )
         : new DynamicFragment()
     if (isHydrating) {
       ;(frag as DynamicFragment).teardown = () => {
index 04b0a4881cbff58337b95aed838bf5d80ad7c350..4dd76021a0884775b4234295628f11dfb1c3dd42 100644 (file)
@@ -14,11 +14,7 @@ import {
 } from '@vue/runtime-dom'
 import { isHydrating } from './dom/hydration'
 import { getInheritedScopeIds } from '@vue/runtime-dom'
-import {
-  type DynamicFragment,
-  type VaporFragment,
-  isFragment,
-} from './fragment'
+import { DynamicFragment, type VaporFragment, isFragment } from './fragment'
 
 export interface TransitionOptions {
   $key?: any
@@ -169,6 +165,19 @@ export function normalizeAnchor(node: Block): Node | undefined {
   }
 }
 
+export function findLastChild(node: Block): Node | undefined | null {
+  if (node && node instanceof Node) {
+    return node
+  } else if (isArray(node)) {
+    return findLastChild(node[node.length - 1])
+  } else if (isVaporComponent(node)) {
+    return findLastChild(node.block!)
+  } else {
+    if (node instanceof DynamicFragment) return node.anchor
+    return findLastChild(node.nodes!)
+  }
+}
+
 /**
  * dev / test only
  */
index 9cf99e38e0c658715f30095bf52e0a4d7c518c14..7bfad15c340abcfc69c253464fecde0754cd1bd5 100644 (file)
@@ -5,6 +5,7 @@ import {
   type BlockFn,
   type TransitionOptions,
   type VaporTransitionHooks,
+  findLastChild,
   insert,
   isValidBlock,
   remove,
@@ -22,7 +23,7 @@ import {
   applyTransitionLeaveHooks,
 } from './components/Transition'
 import type { VaporComponentInstance } from './component'
-import { normalizeAnchor } from './block'
+import { ELSE_IF_ANCHOR_LABEL } from '@vue/shared'
 
 export class VaporFragment<T extends Block = Block>
   implements TransitionOptions
@@ -76,7 +77,7 @@ export class DynamicFragment extends VaporFragment {
 
   update(render?: BlockFn, key: any = render): void {
     if (key === this.current) {
-      if (isHydrating && this.anchorLabel) this.hydrate(this.anchorLabel!, true)
+      if (isHydrating) this.hydrate(this.anchorLabel!, true)
       return
     }
     this.current = key
@@ -142,49 +143,43 @@ export class DynamicFragment extends VaporFragment {
 
     setActiveSub(prevSub)
 
-    if (isHydrating && this.anchorLabel) {
-      // skip hydration for empty forwarded slots because
-      // the server output does not include their anchors
-      if (
-        this.nodes instanceof DynamicFragment &&
-        this.nodes.forwarded &&
-        !isValidBlock(this.nodes)
-      ) {
-        return
-      }
-      this.hydrate(this.anchorLabel)
-    }
+    if (isHydrating) this.hydrate(this.anchorLabel!)
   }
 
   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) {
-      this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, '')!
+    const createAnchor = () => {
+      const { parentNode, nextSibling } = findLastChild(this.nodes)!
+      parentNode!.insertBefore(
+        (this.anchor = createComment(label)),
+        nextSibling,
+      )
+    }
+
+    // manually create anchors for:
+    // 1. else-if branch
+    // 2. empty forwarded slot
+    // (not present in SSR output)
+    if (
+      label === ELSE_IF_ANCHOR_LABEL ||
+      (this.nodes instanceof DynamicFragment &&
+        this.nodes.forwarded &&
+        !isValidBlock(this.nodes))
+    ) {
+      createAnchor()
     } else {
-      this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
-      // comment anchors are not included in ssr slot vnode fallback
-      if (!this.anchor) {
-        if (label === 'slot') {
+      // 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) {
+        this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, '')!
+      } else {
+        this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+        if (!this.anchor && label === 'slot') {
           // fallback to fragment end anchor for
           this.anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
-        } else {
-          // create anchor
-          if (isFragment(this.nodes) && this.nodes.anchor) {
-            // nested vapor fragment
-            const { parentNode, nextSibling } = this.nodes.anchor
-            parentNode!.insertBefore(
-              (this.anchor = __DEV__ ? createComment(label) : createTextNode()),
-              nextSibling,
-            )
-          } else {
-            const { parentNode, nextSibling } = normalizeAnchor(this.nodes)!
-            parentNode!.insertBefore(
-              (this.anchor = __DEV__ ? createComment(label) : createTextNode()),
-              nextSibling,
-            )
-          }
         }
+
+        // anchors are not present in ssr slot vnode fallback
+        if (!this.anchor) createAnchor()
       }
     }
 
index f807ee169db17db8f0bec8216ea41ffb10b39e24..d03409a326800235b521115d0605732724e6cbec 100644 (file)
@@ -2,6 +2,7 @@ export const DYNAMIC_START_ANCHOR_LABEL = '[['
 export const DYNAMIC_END_ANCHOR_LABEL = ']]'
 
 export const IF_ANCHOR_LABEL: string = 'if'
+export const ELSE_IF_ANCHOR_LABEL: string = 'else-if'
 export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
 export const FOR_ANCHOR_LABEL: string = 'for'
 export const SLOT_ANCHOR_LABEL: string = 'slot'