فهرست منبع

fix(runtime-vapor): defer interop slot hydration until fallback is attached

daiwei 1 هفته پیش
والد
کامیت
fd34dcd9c0

+ 98 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -9201,6 +9201,104 @@ describe('VDOM interop', () => {
     )
   })
 
+  test('hydrate interop vapor forwarded empty named slot with multi-root fallback', async () => {
+    const data = reactive({
+      banner: 'banner',
+      title: 'Vue.js',
+    })
+    const { container } = await testWithVDOMApp(
+      `<script setup>
+        const data = _data
+        const components = _components
+      </script>
+      <template>
+        <components.Layout>
+          <template #banner>
+            <div>{{ data.banner }}</div>
+          </template>
+        </components.Layout>
+      </template>`,
+      {
+        Layout: {
+          code: `<script setup>
+            const components = _components
+          </script>
+          <template>
+            <div>
+              <slot name="banner" />
+              <components.Nav>
+                <template #navbar-title>
+                  <slot name="navbar-title" />
+                </template>
+              </components.Nav>
+            </div>
+          </template>`,
+          vapor: true,
+        },
+        Nav: {
+          code: `<script setup>
+            const components = _components
+          </script>
+          <template>
+            <header>
+              <components.NavBar>
+                <template #navbar-title>
+                  <slot name="navbar-title" />
+                </template>
+              </components.NavBar>
+            </header>
+          </template>`,
+          vapor: true,
+        },
+        NavBar: {
+          code: `<script setup>
+            const components = _components
+          </script>
+          <template>
+            <div>
+              <components.NavBarTitle>
+                <template #navbar-title>
+                  <slot name="navbar-title" />
+                </template>
+              </components.NavBarTitle>
+            </div>
+          </template>`,
+          vapor: true,
+        },
+        NavBarTitle: {
+          code: `<script setup>
+            const data = _data
+          </script>
+          <template>
+            <a>
+              <slot name="navbar-title">
+                <svg><path /></svg>
+                <span>{{ data.title }}</span>
+              </slot>
+            </a>
+          </template>`,
+          vapor: true,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toContain(
+      '<!--[--><div>banner</div><!--]-->',
+    )
+    expect(formatHtml(container.innerHTML)).toContain(
+      '<!--[--><svg><path></path></svg><span>Vue.js</span><!--]-->',
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.title = 'Vapor'
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toContain(
+      '<!--[--><svg><path></path></svg><span>Vapor</span><!--]-->',
+    )
+  })
+
   test('hydrate forwarded slot fallback with nested component before parent close marker', async () => {
     const data = ref('foo')
     const { container } = await testWithVaporApp(

+ 5 - 1
packages/runtime-vapor/src/componentSlots.ts

@@ -24,6 +24,7 @@ import {
   type DynamicFragment,
   SlotFragment,
   type VaporFragment,
+  deferSlotHydration,
 } from './fragment'
 import { createElement } from './dom/node'
 import { setDynamicProps } from './dom/prop'
@@ -295,7 +296,10 @@ export function createSlot(
     if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor)
   } else {
     if (fragment.insert) {
-      ;(fragment as VaporFragment).hydrate!()
+      const vaporFragment = fragment as VaporFragment
+      if (!deferSlotHydration(vaporFragment)) {
+        vaporFragment.hydrate!()
+      }
     }
     if (_isLastInsertion) {
       advanceHydrationNode(_insertionParent!)

+ 35 - 0
packages/runtime-vapor/src/fragment.ts

@@ -569,6 +569,27 @@ function setCurrentSlotEndAnchor(end: Node | null): Node | null {
 //   must create its own anchor instead of reusing the slot end anchor.
 export let currentEmptyFragment: DynamicFragment | null | undefined
 
+// VDOM interop slot fragments hydrate eagerly in createSlot(). When they are
+// created while resolving a vapor slot fallback, defer that hydration until
+// the outer slot has attached the final fallback chain.
+let currentDeferredSlotHydrations: VaporFragment[] | null = null
+
+export function deferSlotHydration(fragment: VaporFragment): boolean {
+  if (!currentDeferredSlotHydrations) return false
+  currentDeferredSlotHydrations.push(fragment)
+  return true
+}
+
+function setCurrentDeferredSlotHydrations(
+  queue: VaporFragment[] | null,
+): VaporFragment[] | null {
+  try {
+    return currentDeferredSlotHydrations
+  } finally {
+    currentDeferredSlotHydrations = queue
+  }
+}
+
 export class SlotFragment extends DynamicFragment {
   forwarded = false
   deferredHydrationBoundary?: () => void
@@ -603,10 +624,21 @@ export class SlotFragment extends DynamicFragment {
       } else {
         const wrapped = () => {
           const prev = currentEmptyFragment
+          let deferredHydrations: VaporFragment[] | null = null
           if (isHydrating) currentEmptyFragment = null
+          if (isHydrating) deferredHydrations = []
+          const prevDeferredHydrations = isHydrating
+            ? setCurrentDeferredSlotHydrations(deferredHydrations)
+            : null
           try {
             let block = render()
             const emptyFrag = attachSlotFallback(block, fallback)
+            if (deferredHydrations && deferredHydrations.length) {
+              setCurrentDeferredSlotHydrations(null)
+              for (const fragment of deferredHydrations) {
+                fragment.hydrate && fragment.hydrate()
+              }
+            }
             if (!isValidBlock(block)) {
               if (isHydrating && emptyFrag instanceof DynamicFragment) {
                 currentEmptyFragment = emptyFrag
@@ -615,6 +647,9 @@ export class SlotFragment extends DynamicFragment {
             }
             return block
           } finally {
+            if (isHydrating) {
+              setCurrentDeferredSlotHydrations(prevDeferredHydrations)
+            }
             if (isHydrating) currentEmptyFragment = prev
           }
         }