]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: v-for hydration
authordaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 09:08:07 +0000 (17:08 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 25 Apr 2025 09:08:07 +0000 (17:08 +0800)
packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts

index 53295fb03d9191f4f1abda61abd208209bb4cebd..73d4331a7d7e6e6a5b6a60521ac24c71b3faf20b 100644 (file)
@@ -15,7 +15,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--for--><!--]-->\`)
       }"
     `)
   })
@@ -33,7 +33,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`</ul>\`)
+        _push(\`<!--for--></ul>\`)
       }"
     `)
   })
@@ -52,6 +52,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
+        _push(\`<!--for-->\`)
         if (false) {
           _push(\`<div></div>\`)
           _push(\`<!--if-->\`)
@@ -75,7 +76,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`</ul>\`)
+        _push(\`<!--for--></ul>\`)
       }"
     `)
   })
@@ -97,7 +98,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`</\${_ctx.someTag}>\`)
+        _push(\`<!--for--></\${_ctx.someTag}>\`)
       }"
     `)
   })
@@ -119,9 +120,11 @@ describe('transition-group', () => {
         _ssrRenderList(10, (i) => {
           _push(\`<div></div>\`)
         })
+        _push(\`<!--for-->\`)
         _ssrRenderList(10, (i) => {
           _push(\`<div></div>\`)
         })
+        _push(\`<!--for-->\`)
         if (_ctx.ok) {
           _push(\`<div>ok</div>\`)
           _push(\`<!--if-->\`)
index 6537eee82877452eb8a16dab7172ad9528122f4a..8276507850f452ca0efc68d298ec1e3df3ed090f 100644 (file)
@@ -13,6 +13,7 @@ import {
   processChildrenAsStatement,
 } from '../ssrCodegenTransform'
 import { SSR_RENDER_LIST } from '../runtimeHelpers'
+import { FOR_ANCHOR_LABEL } from '@vue/shared'
 
 // Plugin for the first transform pass, which simply constructs the AST node
 export const ssrTransformFor: NodeTransform =
@@ -48,5 +49,8 @@ export function ssrProcessFor(
   )
   if (!disableNestedFragments) {
     context.pushStringPart(`<!--]-->`)
+  } else {
+    // add anchor for non-fragment v-for
+    context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
   }
 }
index a37076ce160c1ae674682eac65b22dc9001720db..c15bd68542ae2e212a144264456e5b28aa065b31 100644 (file)
@@ -1123,6 +1123,73 @@ describe('Vapor Mode hydration', () => {
         })
       })
 
+      test('on fragment component', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <components.Child v-if="data"/>
+              </div>
+            </template>`,
+            {
+              Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+            },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<!--[--><div>true</div>-true-<!--]-->` +
+              `<!--if-->` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` + `<!--[--><!--]-->` + `<!--${anchorLabel}-->` + `</div>`,
+          )
+        })
+      })
+
+      test('on fragment component with anchor insertion', async () => {
+        runWithEnv(isProd, async () => {
+          const data = ref(true)
+          const { container } = await testHydration(
+            `<template>
+              <div>
+                <span/>
+                <components.Child v-if="data"/>
+                <span/>
+              </div>
+            </template>`,
+            {
+              Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+            },
+            data,
+          )
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--[--><div>true</div>-true-<!--]-->` +
+              `<!--if-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+
+          data.value = false
+          await nextTick()
+          expect(container.innerHTML).toBe(
+            `<div>` +
+              `<span></span>` +
+              `<!--[--><!--]-->` +
+              `<!--${anchorLabel}-->` +
+              `<span></span>` +
+              `</div>`,
+          )
+        })
+      })
+
       test('consecutive v-if on fragment component with anchor insertion', async () => {
         runWithEnv(isProd, async () => {
           const data = ref(true)
@@ -1311,7 +1378,168 @@ describe('Vapor Mode hydration', () => {
     }
   })
 
-  test.todo('for')
+  describe('for', () => {
+    test('basic v-for', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<!--]-->` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `</div>`,
+      )
+    })
+
+    test('v-for with text node', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div><!--[--><span>a</span><span>b</span><span>c</span><!--]--></div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div><!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]--></div>`,
+      )
+    })
+
+    test('v-for with anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span/>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<!--]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
+
+    test('consecutive v-for with anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span/>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<!--]-->` +
+          `<!--[[-->` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<!--]-->` +
+          `<!--]]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<!--[[-->` +
+          `<!--[-->` +
+          `<span>a</span>` +
+          `<span>b</span>` +
+          `<span>c</span>` +
+          `<span>d</span>` +
+          `<!--]-->` +
+          `<!--]]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
+
+    // TODO wait for slots hydration support
+    test.todo('v-for on component', async () => {})
+
+    // TODO wait for slots hydration support
+    test.todo('on fragment component', async () => {})
+
+    // TODO wait for vapor TransitionGroup support
+    // v-for inside TransitionGroup does not render as a fragment
+    test.todo('v-for in TransitionGroup', async () => {})
+  })
 
   test.todo('slots')
 
index 0cd8317532f4fb93790775321bf2015fbe2f09da..b8016a64de2ccb28b4b58ada382a5b1517b07481 100644 (file)
@@ -9,8 +9,14 @@ import {
   shallowRef,
   toReactive,
 } from '@vue/reactivity'
-import { getSequence, isArray, isObject, isString } from '@vue/shared'
-import { createComment, createTextNode } from './dom/node'
+import {
+  FOR_ANCHOR_LABEL,
+  getSequence,
+  isArray,
+  isObject,
+  isString,
+} from '@vue/shared'
+import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
 import {
   type Block,
   VaporFragment,
@@ -22,8 +28,17 @@ import { currentInstance, isVaporComponent } from './component'
 import type { DynamicSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
 import { VaporVForFlags } from '../../shared/src/vaporFlags'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { insertionAnchor, insertionParent } from './insertionState'
+import {
+  currentHydrationNode,
+  isComment,
+  isHydrating,
+  locateHydrationNode,
+} from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
 
 class ForBlock extends VaporFragment {
   scope: EffectScope | undefined
@@ -71,15 +86,24 @@ export const createFor = (
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
   if (isHydrating) {
-    locateHydrationNode()
+    locateHydrationNode(true)
+  } else {
+    resetInsertionState()
   }
 
   let isMounted = false
   let oldBlocks: ForBlock[] = []
   let newBlocks: ForBlock[]
   let parent: ParentNode | undefined | null
-  // TODO handle this in hydration
-  const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+  const parentAnchor = isHydrating
+    ? // Use fragment end anchor if available, otherwise use the specific for anchor.
+      nextSiblingAnchor(
+        currentHydrationNode!,
+        isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
+      )!
+    : __DEV__
+      ? createComment('for')
+      : createTextNode()
   const frag = new VaporFragment(oldBlocks)
   const instance = currentInstance!
   const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
index b0e8e7d5283d64979ecf3d4184f80924edf246b4..4588f4887ab2354998f5504341820eb66132bb38 100644 (file)
@@ -10,7 +10,6 @@ import {
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
   next,
-  prev,
 } from './node'
 import { isDynamicFragmentEndAnchor } from '@vue/shared'
 
@@ -98,7 +97,7 @@ function locateHydrationNodeImpl(isFragment?: boolean) {
     // if the last child is a comment, it is the anchor for the fragment
     // so it need to find the previous node
     if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
-      let previous = prev(node)
+      let previous = node.previousSibling //prev(node)
       if (previous) node = previous
     }
 
index 2a546c69e2facc098b764fcc03ae0e312350e1b3..6b9b63c5c2f9fc2d4cc682317a7f56f85edc6d76 100644 (file)
@@ -105,6 +105,7 @@ export function disableHydrationNodeLookup(): void {
 }
 
 /*! #__NO_SIDE_EFFECTS__ */
+// TODO check if this is still needed
 export function prev(node: Node): Node | null {
   // process dynamic node (<!--[[-->...<!--]]-->) as a single one
   if (isComment(node, DYNAMIC_END_ANCHOR_LABEL)) {
@@ -145,6 +146,9 @@ export function nextSiblingAnchor(
   anchorLabel: string,
 ): Comment | null {
   node = handleWrappedNode(node)
+  if (isComment(node, anchorLabel)) {
+    return node as Comment
+  }
 
   let n = node.nextSibling
   while (n) {