]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: refactor
authordaiwei <daiwei521@126.com>
Sat, 26 Apr 2025 02:34:46 +0000 (10:34 +0800)
committerdaiwei <daiwei521@126.com>
Sat, 26 Apr 2025 03:03:11 +0000 (11:03 +0800)
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/runtime-core/src/hydration.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/shared/src/domAnchors.ts

index e2324004044a098ec83c179b5376c2e6f39fedf0..56f38b9e5852494c3a5e62cfd036c0b71d566a0f 100644 (file)
@@ -302,8 +302,8 @@ function processChildrenDynamicInfo(
     const child = filteredChildren[i]
     if (
       isStaticChildNode(child) ||
-      // v-if has an anchor, which can be used to distinguish the boundary
-      child.type === NodeTypes.IF
+      // fragment has it's own anchor, which can be used to distinguish the boundary
+      isFragmentChild(child)
     ) {
       continue
     }
@@ -359,10 +359,10 @@ function processChildrenDynamicInfo(
 }
 
 /**
- * Check if a node should be processed as dynamic.
+ * Check if a node should be processed as dynamic child.
  * This is primarily used in Vapor mode hydration to wrap dynamic parts
  * with markers (`<!--[[-->` and `<!--]]-->`).
- * The purpose is to distinguish the boundaries of nodes during hydration
+ * The purpose is to distinguish the boundaries of nodes during vapor hydration
  *
  * 1. two consecutive dynamic nodes should only wrap the second one
  * <element>
@@ -416,3 +416,8 @@ function shouldProcessChildAsDynamic(
 
   return false
 }
+
+function isFragmentChild(child: TemplateChildNode): boolean {
+  const { type } = child
+  return type === NodeTypes.IF || type === NodeTypes.FOR
+}
index 7c7fdf3860894fbf9d81abcca16c5e562555fae3..4c8be2281d372525987fb45a742e2ca205820af8 100644 (file)
@@ -26,13 +26,13 @@ import {
   includeBooleanAttr,
   isBooleanAttr,
   isDynamicAnchor,
-  isDynamicFragmentEndAnchor,
   isKnownHtmlAttr,
   isKnownSvgAttr,
   isOn,
   isRenderableAttrValue,
   isReservedProp,
   isString,
+  isVaporFragmentEndAnchor,
   normalizeClass,
   normalizeStyle,
   stringifyStyle,
@@ -126,7 +126,7 @@ export function createHydrationFunctions(
     // skip if:
     // - dynamic anchors (`<!--[[-->`, `<!--][-->`)
     // - dynamic fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
-    if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
+    if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
       n = next(n)
     }
     return n
@@ -158,7 +158,7 @@ export function createHydrationFunctions(
     slotScopeIds: string[] | null,
     optimized = false,
   ): Node | null => {
-    if (isDynamicAnchor(node) || isDynamicFragmentEndAnchor(node)) {
+    if (isDynamicAnchor(node) || isVaporFragmentEndAnchor(node)) {
       node = nextSibling(node)!
     }
     optimized = optimized || !!vnode.dynamicChildren
index c15bd68542ae2e212a144264456e5b28aa065b31..d8c40bdc6b251f407c4d3dbaffd4335df4d4c68d 100644 (file)
@@ -79,15 +79,6 @@ const triggerEvent = (type: string, el: Element) => {
   el.dispatchEvent(event)
 }
 
-async function runWithEnv(isProd: boolean, fn: () => Promise<void>) {
-  if (isProd) __DEV__ = false
-  try {
-    await fn()
-  } finally {
-    if (isProd) __DEV__ = true
-  }
-}
-
 describe('Vapor Mode hydration', () => {
   delegateEvents('click')
 
@@ -95,7 +86,7 @@ describe('Vapor Mode hydration', () => {
     document.body.innerHTML = ''
   })
 
-  describe('element', () => {
+  describe('text', () => {
     test('root text', async () => {
       const { data, container } = await testHydration(`
       <template>{{ data }}</template>
@@ -107,6 +98,10 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toMatchInlineSnapshot(`"bar"`)
     })
 
+    test.todo('consecutive text nodes', () => {})
+  })
+
+  describe('element', () => {
     test('root comment', async () => {
       const { container } = await testHydration(`
       <template><!----></template>
@@ -635,13 +630,13 @@ describe('Vapor Mode hydration', () => {
     test('consecutive fragment components with anchor insertion', async () => {
       const { container, data } = await testHydration(
         `<template>
-        <div>
-          <span/>
-          <components.Child/>
-          <components.Child/>
-          <span/>
-        </div>
-      </template>
+          <div>
+            <span/>
+            <components.Child/>
+            <components.Child/>
+            <span/>
+          </div>
+        </template>
       `,
         {
           Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
@@ -864,518 +859,532 @@ describe('Vapor Mode hydration', () => {
     })
   })
 
-  describe('if', () => {
-    describe('DEV mode', () => {
-      runTests()
+  describe('dynamic component', () => {
+    const anchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
+
+    test('basic dynamic component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <component :is="components[data]"/>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
     })
 
-    describe('PROD mode', () => {
-      runTests(true)
+    test('dynamic component with anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components[data]"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<div>foo</div><!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<div>bar</div><!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
     })
 
-    function runTests(isProd: boolean = false) {
-      const anchorLabel = IF_ANCHOR_LABEL
-
-      test('basic toggle - true -> false', async () => {
-        runWithEnv(isProd, async () => {
-          const data = ref(true)
-          const { container } = await testHydration(
-            `<template>
-              <div v-if="data">foo</div>
-            </template>`,
-            undefined,
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>foo</div><!--${anchorLabel}-->`,
-          )
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
-        })
-      })
-
-      test('basic toggle - false -> true', async () => {
-        runWithEnv(isProd, async () => {
-          if (isProd) __DEV__ = false
-          const data = ref(false)
-          const { container } = await testHydration(
-            `<template>
-              <div v-if="data">foo</div>
-            </template>`,
-            undefined,
-            data,
-          )
-          // v-if="false" is rendered as <!----> in the server-rendered HTML
-          // it reused as anchor, so the anchor label is empty
-          expect(container.innerHTML).toBe(`<!---->`)
-
-          data.value = true
-          await nextTick()
-          expect(container.innerHTML).toBe(`<div>foo</div><!---->`)
-        })
-      })
-
-      test('v-if/else-if/else chain - switch branches', async () => {
-        runWithEnv(isProd, async () => {
-          const data = ref('a')
-          const { container } = await testHydration(
-            `<template>
-              <div v-if="data === 'a'">foo</div>
-              <div v-else-if="data === 'b'">bar</div>
-              <div v-else>baz</div>
-            </template>`,
-            undefined,
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>foo</div><!--${anchorLabel}-->`,
-          )
-
-          data.value = 'b'
-          await nextTick()
-          // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
-          // so it won't be rendered
-          expect(container.innerHTML).toBe(
-            `<div>bar</div><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
-          )
-
-          data.value = 'c'
-          await nextTick()
-          // same as above
-          expect(container.innerHTML).toBe(
-            `<div>baz</div><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
-          )
-
-          data.value = 'a'
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>foo</div><!--${anchorLabel}-->`,
-          )
-        })
-      })
-
-      test('nested if', async () => {
-        runWithEnv(isProd, async () => {
-          const data = reactive({ outer: true, inner: true })
-          const { container } = await testHydration(
-            `<template>
-              <div v-if="data.outer">
-                <span>outer</span>
-                <div v-if="data.inner">inner</div>
-              </div>
-            </template>`,
-            undefined,
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span>outer</span>` +
-              `<div>inner</div><!--${anchorLabel}-->` +
-              `</div><!--${anchorLabel}-->`,
-          )
-
-          data.inner = false
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span>outer</span>` +
-              `<!--${anchorLabel}-->` +
-              `</div><!--${anchorLabel}-->`,
-          )
-
-          data.outer = false
-          await nextTick()
-          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
-        })
-      })
-
-      test('on component', async () => {
-        runWithEnv(isProd, async () => {
-          const data = ref(true)
-          const { container } = await testHydration(
-            `<template>
-              <components.Child v-if="data"/>
-            </template>`,
-            { Child: `<template>foo</template>` },
-            data,
-          )
-          expect(container.innerHTML).toBe(`foo<!--${anchorLabel}-->`)
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
-        })
-      })
-
-      test('v-if/else-if/else chain on component - switch branches', async () => {
-        runWithEnv(isProd, async () => {
-          const data = ref('a')
-          const { container } = await testHydration(
-            `<template>
-              <components.Child1 v-if="data === 'a'"/>
-              <components.Child2 v-else-if="data === 'b'"/>
-              <components.Child3 v-else/>
-            </template>`,
-            {
-              Child1: `<template><span>{{data}} child1</span></template>`,
-              Child2: `<template><span>{{data}} child2</span></template>`,
-              Child3: `<template><span>{{data}} child3</span></template>`,
-            },
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<span>a child1</span><!--${anchorLabel}-->`,
-          )
-
-          data.value = 'b'
-          await nextTick()
-          // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
-          // so it won't be rendered
-          expect(container.innerHTML).toBe(
-            `<span>b child2</span><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
-          )
-
-          data.value = 'c'
-          await nextTick()
-          // same as above
-          expect(container.innerHTML).toBe(
-            `<span>c child3</span><!--${anchorLabel}-->${isProd ? '' : `<!--${anchorLabel}-->`}`,
-          )
-
-          data.value = 'a'
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<span>a child1</span><!--${anchorLabel}-->`,
-          )
-        })
-      })
-
-      test('on 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>foo</template>` },
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `foo<!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-
-      test('consecutive v-if on component with anchor insertion', async () => {
-        runWithEnv(isProd, async () => {
-          const data = ref(true)
-          const { container } = await testHydration(
-            `<template>
-              <div>
-                <span/>
-                <components.Child v-if="data"/>
-                <components.Child v-if="data"/>
-                <span/>
-              </div>
-            </template>`,
-            { Child: `<template>foo</template>` },
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `foo<!--${anchorLabel}-->` +
-              `foo<!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<!--${anchorLabel}-->` +
-              `<!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-
-      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)
-          const { container } = await testHydration(
-            `<template>
-              <div>
-                <span/>
-                <components.Child v-if="data"/>
-                <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-<!--]--><!--${anchorLabel}-->` +
-              `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<!--[--><!--]--><!--${anchorLabel}-->` +
-              `<!--[--><!--]--><!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-
-      test('on dynamic component with anchor insertion', async () => {
-        runWithEnv(isProd, async () => {
-          const dynamicComponentAnchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
-          const data = ref(true)
-          const { container } = await testHydration(
-            `<template>
-              <div>
-                <span/>
-                <component :is="components.Child" v-if="data"/>
-                <span/>
-              </div>
-            </template>`,
-            { Child: `<template>foo</template>` },
-            data,
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `foo<!--${dynamicComponentAnchorLabel}--><!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = false
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-    }
+    test('consecutive dynamic components with anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components[data]"/>
+            <component :is="components[data]"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<div>foo</div><!--${anchorLabel}-->` +
+          `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<div>bar</div><!--${anchorLabel}-->` +
+          `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
   })
 
-  describe('dynamic component', () => {
-    describe('DEV mode', () => {
-      runTests()
+  describe('if', () => {
+    const anchorLabel = IF_ANCHOR_LABEL
+
+    test('basic toggle - true -> false', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">foo</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+    })
+
+    test('basic toggle - false -> true', async () => {
+      const data = ref(false)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">foo</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      // v-if="false" is rendered as <!----> in the server-rendered HTML
+      // it reused as anchor, so the anchor label is empty
+      expect(container.innerHTML).toBe(`<!---->`)
+
+      data.value = true
+      await nextTick()
+      expect(container.innerHTML).toBe(`<div>foo</div><!---->`)
     })
-    describe('PROD mode', () => {
-      runTests(true)
+
+    test('v-if/else-if/else chain - switch branches', async () => {
+      const data = ref('a')
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data === 'a'">foo</div>
+          <div v-else-if="data === 'b'">bar</div>
+          <div v-else>baz</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+
+      data.value = 'b'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>bar</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
+
+      data.value = 'c'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>baz</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
+
+      data.value = 'a'
+      await nextTick()
+      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
     })
 
-    function runTests(isProd: boolean = false) {
-      const anchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
-
-      test('basic dynamic component', async () => {
-        runWithEnv(isProd, async () => {
-          const { container, data } = await testHydration(
-            `<template>
-              <component :is="components[data]"/>
-            </template>`,
-            {
-              foo: `<template><div>foo</div></template>`,
-              bar: `<template><div>bar</div></template>`,
-            },
-            ref('foo'),
-          )
-          expect(container.innerHTML).toBe(
-            `<div>foo</div><!--${anchorLabel}-->`,
-          )
-
-          data.value = 'bar'
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>bar</div><!--${anchorLabel}-->`,
-          )
-        })
-      })
-
-      test('dynamic component with anchor insertion', async () => {
-        runWithEnv(isProd, async () => {
-          const { container, data } = await testHydration(
-            `<template>
-            <div>
-              <span/>
-              <component :is="components[data]"/>
-              <span/>
-            </div>
+    test('v-if/else-if/else chain - switch branches (PROD)', async () => {
+      try {
+        __DEV__ = false
+        const data = ref('a')
+        const { container } = await testHydration(
+          `<template>
+            <div v-if="data === 'a'">foo</div>
+            <div v-else-if="data === 'b'">bar</div>
+            <div v-else>baz</div>
           </template>`,
-            {
-              foo: `<template><div>foo</div></template>`,
-              bar: `<template><div>bar</div></template>`,
-            },
-            ref('foo'),
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<div>foo</div><!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = 'bar'
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<div>bar</div><!--${anchorLabel}-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-
-      test('consecutive dynamic components with anchor insertion', async () => {
-        runWithEnv(isProd, async () => {
-          const { container, data } = await testHydration(
-            `<template>
-              <div>
-                <span/>
-                <component :is="components[data]"/>
-                <component :is="components[data]"/>
-                <span/>
-              </div>
-            </template>`,
-            {
-              foo: `<template><div>foo</div></template>`,
-              bar: `<template><div>bar</div></template>`,
-            },
-            ref('foo'),
-          )
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<div>foo</div><!--${anchorLabel}-->` +
-              `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-
-          data.value = 'bar'
-          await nextTick()
-          expect(container.innerHTML).toBe(
-            `<div>` +
-              `<span></span>` +
-              `<div>bar</div><!--${anchorLabel}-->` +
-              `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
-              `<span></span>` +
-              `</div>`,
-          )
-        })
-      })
-    }
+          undefined,
+          data,
+        )
+        expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+
+        data.value = 'b'
+        await nextTick()
+        // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
+        // so it won't be rendered
+        expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
+
+        data.value = 'c'
+        await nextTick()
+        // same as above
+        expect(container.innerHTML).toBe(`<div>baz</div><!--${anchorLabel}-->`)
+
+        data.value = 'a'
+        await nextTick()
+        expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+      } finally {
+        __DEV__ = true
+      }
+    })
+
+    test('nested if', async () => {
+      const data = reactive({ outer: true, inner: true })
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data.outer">
+            <span>outer</span>
+            <div v-if="data.inner">inner</div>
+          </div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span>outer</span>` +
+          `<div>inner</div><!--${anchorLabel}-->` +
+          `</div><!--${anchorLabel}-->`,
+      )
+
+      data.inner = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span>outer</span>` +
+          `<!--${anchorLabel}-->` +
+          `</div><!--${anchorLabel}-->`,
+      )
+
+      data.outer = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+    })
+
+    test('on component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <components.Child v-if="data"/>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toBe(`foo<!--${anchorLabel}-->`)
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+    })
+
+    test('v-if/else-if/else chain on component - switch branches', async () => {
+      const data = ref('a')
+      const { container } = await testHydration(
+        `<template>
+          <components.Child1 v-if="data === 'a'"/>
+          <components.Child2 v-else-if="data === 'b'"/>
+          <components.Child3 v-else/>
+        </template>`,
+        {
+          Child1: `<template><span>{{data}} child1</span></template>`,
+          Child2: `<template><span>{{data}} child2</span></template>`,
+          Child3: `<template><span>{{data}} child3</span></template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<span>a child1</span><!--${anchorLabel}-->`,
+      )
+
+      data.value = 'b'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<span>b child2</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
+
+      data.value = 'c'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<span>c child3</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      )
+
+      data.value = 'a'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<span>a child1</span><!--${anchorLabel}-->`,
+      )
+    })
+
+    test('v-if/else-if/else chain on component - switch branches (PROD)', async () => {
+      try {
+        __DEV__ = false
+        const data = ref('a')
+        const { container } = await testHydration(
+          `<template>
+            <components.Child1 v-if="data === 'a'"/>
+            <components.Child2 v-else-if="data === 'b'"/>
+            <components.Child3 v-else/>
+          </template>`,
+          {
+            Child1: `<template><span>{{data}} child1</span></template>`,
+            Child2: `<template><span>{{data}} child2</span></template>`,
+            Child3: `<template><span>{{data}} child3</span></template>`,
+          },
+          data,
+        )
+        expect(container.innerHTML).toBe(
+          `<span>a child1</span><!--${anchorLabel}-->`,
+        )
+
+        data.value = 'b'
+        await nextTick()
+        // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
+        // so it won't be rendered
+        expect(container.innerHTML).toBe(
+          `<span>b child2</span><!--${anchorLabel}-->`,
+        )
+
+        data.value = 'c'
+        await nextTick()
+        // same as above
+        expect(container.innerHTML).toBe(
+          `<span>c child3</span><!--${anchorLabel}-->`,
+        )
+
+        data.value = 'a'
+        await nextTick()
+        expect(container.innerHTML).toBe(
+          `<span>a child1</span><!--${anchorLabel}-->`,
+        )
+      } finally {
+        __DEV__ = true
+      }
+    })
+
+    test('on component with anchor insertion', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `foo<!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
+
+    test('consecutive v-if on component with anchor insertion', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `foo<!--${anchorLabel}-->` +
+          `foo<!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--${anchorLabel}-->` +
+          `<!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
+
+    test('on fragment component', 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 () => {
+      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 () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <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-<!--]--><!--${anchorLabel}-->` +
+          `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[--><!--]--><!--${anchorLabel}-->` +
+          `<!--[--><!--]--><!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
+
+    test('on dynamic component with anchor insertion', async () => {
+      const dynamicComponentAnchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components.Child" v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `foo<!--${dynamicComponentAnchorLabel}--><!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--${anchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
   })
 
   describe('for', () => {
@@ -1495,13 +1504,11 @@ describe('Vapor Mode hydration', () => {
           `<span>b</span>` +
           `<span>c</span>` +
           `<!--]-->` +
-          `<!--[[-->` +
           `<!--[-->` +
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
           `<!--]-->` +
-          `<!--]]-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1517,14 +1524,12 @@ describe('Vapor Mode hydration', () => {
           `<span>c</span>` +
           `<span>d</span>` +
           `<!--]-->` +
-          `<!--[[-->` +
           `<!--[-->` +
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
           `<span>d</span>` +
           `<!--]-->` +
-          `<!--]]-->` +
           `<span></span>` +
           `</div>`,
       )
index b8016a64de2ccb28b4b58ada382a5b1517b07481..54b65d50bdf6c89ca2e61173501d04094207e4b8 100644 (file)
@@ -16,7 +16,11 @@ import {
   isObject,
   isString,
 } from '@vue/shared'
-import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
+import {
+  createComment,
+  createTextNode,
+  nextVaporFragmentAnchor,
+} from './dom/node'
 import {
   type Block,
   VaporFragment,
@@ -97,7 +101,7 @@ export const createFor = (
   let parent: ParentNode | undefined | null
   const parentAnchor = isHydrating
     ? // Use fragment end anchor if available, otherwise use the specific for anchor.
-      nextSiblingAnchor(
+      nextVaporFragmentAnchor(
         currentHydrationNode!,
         isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
       )!
index c146bb10e8f020bfe4369777c368c439bacd84f0..c0727cb5d91e454c1ec3cdc4dde7570b313b45d9 100644 (file)
@@ -1,11 +1,15 @@
-import { isArray, isDynamicFragmentEndAnchor } from '@vue/shared'
+import { isArray, isVaporFragmentEndAnchor } from '@vue/shared'
 import {
   type VaporComponentInstance,
   isVaporComponent,
   mountComponent,
   unmountComponent,
 } from './component'
-import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
+import {
+  createComment,
+  createTextNode,
+  nextVaporFragmentAnchor,
+} from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import {
   currentHydrationNode,
@@ -95,8 +99,8 @@ export class DynamicFragment extends VaporFragment {
       this.anchor = currentHydrationNode
     } else {
       // find next sibling dynamic fragment end anchor
-      const anchor = nextSiblingAnchor(currentHydrationNode!, label)!
-      if (anchor && isDynamicFragmentEndAnchor(anchor)) {
+      const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)!
+      if (anchor && isVaporFragmentEndAnchor(anchor)) {
         this.anchor = anchor
       } else if (__DEV__) {
         // TODO warning
index 4588f4887ab2354998f5504341820eb66132bb38..f290954b7f10e254c0d1d830684989211b783ef5 100644 (file)
@@ -11,7 +11,7 @@ import {
   enableHydrationNodeLookup,
   next,
 } from './node'
-import { isDynamicFragmentEndAnchor } from '@vue/shared'
+import { isVaporFragmentEndAnchor } from '@vue/shared'
 
 export let isHydrating = false
 export let currentHydrationNode: Node | null = null
@@ -83,7 +83,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
   return node
 }
 
-function locateHydrationNodeImpl(isFragment?: boolean) {
+function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
   let node: Node | null
   // prepend / firstChild
   if (insertionAnchor === 0) {
@@ -94,10 +94,9 @@ function locateHydrationNodeImpl(isFragment?: boolean) {
   } else {
     node = insertionParent ? insertionParent.lastChild : currentHydrationNode
 
-    // 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 = node.previousSibling //prev(node)
+    // if the last child is a vapor fragment end anchor, find the previous one
+    if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+      let previous = node.previousSibling
       if (previous) node = previous
     }
 
@@ -166,25 +165,3 @@ export function locateEndAnchor(
   }
   return null
 }
-
-export function locateStartAnchor(
-  node: Node | null,
-  open = '[',
-  close = ']',
-): Node | null {
-  let match = 0
-  while (node) {
-    if (node.nodeType === 8) {
-      if ((node as Comment).data === close) match++
-      if ((node as Comment).data === open) {
-        if (match === 0) {
-          return node
-        } else {
-          match--
-        }
-      }
-    }
-    node = node.previousSibling
-  }
-  return null
-}
index 6b9b63c5c2f9fc2d4cc682317a7f56f85edc6d76..18cfd3fc903e304dc5ee37463a001b9d95be0660 100644 (file)
@@ -1,14 +1,9 @@
-import {
-  isComment,
-  isEmptyText,
-  locateEndAnchor,
-  locateStartAnchor,
-} from './hydration'
+import { isComment, isEmptyText, locateEndAnchor } from './hydration'
 import {
   DYNAMIC_END_ANCHOR_LABEL,
   DYNAMIC_START_ANCHOR_LABEL,
   isDynamicAnchor,
-  isDynamicFragmentEndAnchor,
+  isVaporFragmentEndAnchor,
 } from '@vue/shared'
 
 /*! #__NO_SIDE_EFFECTS__ */
@@ -104,30 +99,6 @@ export function disableHydrationNodeLookup(): void {
   nthChild.impl = _nthChild
 }
 
-/*! #__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)) {
-    node = locateStartAnchor(
-      node,
-      DYNAMIC_START_ANCHOR_LABEL,
-      DYNAMIC_END_ANCHOR_LABEL,
-    )!
-  }
-
-  // process fragment node (<!--[-->...<!--]-->) as a single one
-  else if (isComment(node, ']')) {
-    node = locateStartAnchor(node)!
-  }
-
-  let n = node.previousSibling
-  while (n && isNonHydrationNode(n)) {
-    n = n.previousSibling
-  }
-  return n
-}
-
 function isNonHydrationNode(node: Node) {
   return (
     // empty text nodes, no need to hydrate
@@ -136,12 +107,12 @@ function isNonHydrationNode(node: Node) {
     isDynamicAnchor(node) ||
     // fragment end anchor (`<!--]-->`)
     isComment(node, ']') ||
-    // dynamic fragment end anchors
-    isDynamicFragmentEndAnchor(node)
+    // vapor fragment end anchors
+    isVaporFragmentEndAnchor(node)
   )
 }
 
-export function nextSiblingAnchor(
+export function nextVaporFragmentAnchor(
   node: Node,
   anchorLabel: string,
 ): Comment | null {
index ae75a97b1b40688dbddcdc8cbaf6558b695d55f8..e93bc01889dab776e973e24447332d640ab0be8a 100644 (file)
@@ -1,12 +1,10 @@
 export const DYNAMIC_START_ANCHOR_LABEL = '[['
 export const DYNAMIC_END_ANCHOR_LABEL = ']]'
 
-export const IF_ANCHOR_LABEL: string = __DEV__ ? 'if' : '$'
-export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = __DEV__
-  ? 'dynamic-component'
-  : '$2'
-export const FOR_ANCHOR_LABEL: string = __DEV__ ? 'for' : '$3'
-export const SLOT_ANCHOR_LABEL: string = __DEV__ ? 'slot' : '$4'
+export const IF_ANCHOR_LABEL: string = 'if'
+export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
+export const FOR_ANCHOR_LABEL: string = 'for'
+export const SLOT_ANCHOR_LABEL: string = 'slot'
 
 export function isDynamicAnchor(node: Node): node is Comment {
   if (node.nodeType !== 8) return false
@@ -17,7 +15,7 @@ export function isDynamicAnchor(node: Node): node is Comment {
   )
 }
 
-export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
+export function isVaporFragmentEndAnchor(node: Node): node is Comment {
   if (node.nodeType !== 8) return false
 
   const data = (node as Comment).data