]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: skip dynamic anchors and empty text nodes
authordaiwei <daiwei521@126.com>
Wed, 23 Apr 2025 02:53:57 +0000 (10:53 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 23 Apr 2025 02:53:57 +0000 (10:53 +0800)
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts

index 790cd9d6fb19038110a5d91ca54f4d62cc7c87a6..da47438c2a88c3d737854ea0eb74cb6c6e1e77c2 100644 (file)
@@ -70,10 +70,23 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
       if (prevDynamics.length) {
         if (hasStaticTemplate) {
-          context.childrenTemplate[index - prevDynamics.length] = `<!>`
-          prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
-          const anchor = (prevDynamics[0].anchor = context.increaseId())
-          registerInsertion(prevDynamics, context, anchor)
+          // each dynamic child gets its own placeholder node.
+          // this makes it easier to locate the corresponding node during hydration.
+          for (let i = 0; i < prevDynamics.length; i++) {
+            const idx = index - prevDynamics.length + i
+            context.childrenTemplate[idx] = `<!>`
+            const dynamicChild = prevDynamics[i]
+            dynamicChild.flags -= DynamicFlag.NON_TEMPLATE
+            const anchor = (dynamicChild.anchor = context.increaseId())
+            if (
+              dynamicChild.operation &&
+              isBlockOperation(dynamicChild.operation)
+            ) {
+              // block types
+              dynamicChild.operation.parent = context.reference()
+              dynamicChild.operation.anchor = anchor
+            }
+          }
         } else {
           registerInsertion(prevDynamics, context, -1 /* prepend */)
         }
index 27ef427a116a8c6b42fd2d0c026ef0db57e699f0..246d8c9b9bea716619160dfc07bcb6125e9eece3 100644 (file)
@@ -317,7 +317,7 @@ describe('Vapor Mode hydration', () => {
     )
   })
 
-  test.todo('mixed component and text with anchor insertion', async () => {
+  test('mixed component and text with anchor insertion', async () => {
     const { container, data } = await testHydration(
       `<template>
         <div>
@@ -333,11 +333,15 @@ describe('Vapor Mode hydration', () => {
         Child: `<template>{{ data }}</template>`,
       },
     )
-    expect(container.innerHTML).toMatchInlineSnapshot(``)
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span><!--[[-->foo<!--]]--><!--[[--> <!--]]--><!--[[--> foo <!--]]--><!--[[--> <!--]]--><!--[[-->foo<!--]]--><span></span></div>"`,
+    )
 
     data.value = 'bar'
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(``)
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div><span></span><!--[[-->bar<!--]]--><!--[[--> <!--]]--><!--[[--> bar <!--]]--><!--[[--> <!--]]--><!--[[-->bar<!--]]--><span></span></div>"`,
+    )
   })
 
   test.todo('if')
index 30560801e560da77c21977b3abd5eb5799d1d069..5d6af553458b67dfe2b1a99875155517bad36a58 100644 (file)
@@ -37,7 +37,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
 export let adoptTemplate: (node: Node, template: string) => Node | null
 export let locateHydrationNode: () => void
 
-const isComment = (node: Node, data: string): node is Anchor =>
+export const isComment = (node: Node, data: string): node is Anchor =>
   node.nodeType === 8 && (node as Comment).data === data
 
 /**
@@ -76,16 +76,8 @@ function locateHydrationNodeImpl() {
   if (insertionAnchor === 0) {
     node = child(insertionParent!)
   } else if (insertionAnchor) {
-    // dynamic anchor `<!--[[-->`
-    if (isDynamicStart(insertionAnchor)) {
-      const anchor = (insertionParent!.$lds = insertionParent!.$lds
-        ? // continuous dynamic children, the next dynamic start must exist
-          locateNextDynamicStart(insertionParent!.$lds)!
-        : insertionAnchor)
-      node = anchor.nextSibling
-    } else {
-      node = insertionAnchor
-    }
+    // for dynamic children, use insertionAnchor as the node
+    node = insertionAnchor
   } else {
     node = insertionParent ? insertionParent.lastChild : currentHydrationNode
     if (node && isComment(node, ']')) {
@@ -127,32 +119,3 @@ function locateHydrationNodeImpl() {
   resetInsertionState()
   currentHydrationNode = node
 }
-
-function isDynamicStart(node: Node): node is Anchor {
-  return isComment(node, '[[')
-}
-
-function locateNextDynamicStart(anchor: Anchor): Anchor | undefined {
-  let cur: Node | null = anchor
-  let end = null
-  let depth = 0
-  while (cur) {
-    cur = cur.nextSibling
-    if (cur) {
-      if (isComment(cur, '[[')) {
-        depth++
-      } else if (isComment(cur, ']]')) {
-        if (!depth) {
-          end = cur
-          break
-        } else {
-          depth--
-        }
-      }
-    }
-  }
-
-  if (end) {
-    return end!.nextSibling as Anchor
-  }
-}
index 83bc32c57f0b0eef5c82cc6da675e3a244ce3edb..7081c5f7048763bbefe145bd19645cb9c27ec620 100644 (file)
@@ -1,4 +1,7 @@
 /*! #__NO_SIDE_EFFECTS__ */
+
+import { isComment, isHydrating } from './hydration'
+
 export function createTextNode(value = ''): Text {
   return document.createTextNode(value)
 }
@@ -25,5 +28,20 @@ export function nthChild(node: Node, i: number): Node {
 
 /*! #__NO_SIDE_EFFECTS__ */
 export function next(node: Node): Node {
-  return node.nextSibling!
+  let n = node.nextSibling!
+  if (isHydrating) {
+    // skip dynamic anchors and empty text nodes
+    while (n && (isDynamicAnchor(n) || isEmptyText(n))) {
+      n = n.nextSibling!
+    }
+  }
+  return n
+}
+
+function isDynamicAnchor(node: Node): node is Comment {
+  return isComment(node, '[[') || isComment(node, ']]')
+}
+
+function isEmptyText(node: Node): node is Text {
+  return node.nodeType === 3 && !(node as Text).data.trim()
 }