Răsfoiți Sursa

fix(keep-alive): support VDOM async components in VaporKeepAlive

daiwei 3 luni în urmă
părinte
comite
6123f9b262

+ 132 - 0
packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts

@@ -1,4 +1,5 @@
 import {
+  defineAsyncComponent,
   h,
   nextTick,
   onActivated,
@@ -1924,6 +1925,137 @@ describe('VaporKeepAlive', () => {
       inputEl = container.firstChild as HTMLInputElement
       expect(inputEl.value).toBe('vdom')
     })
+
+    test('should cache interop async component and match by resolved name', async () => {
+      const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
+
+      const InnerComp = {
+        name: 'InnerComp',
+        setup() {
+          onActivated(() => oneHooks.activated())
+          onDeactivated(() => oneHooks.deactivated())
+          return () => h('div', 'async inner')
+        },
+      }
+
+      const AsyncComp = defineAsyncComponent(
+        () =>
+          new Promise(resolve =>
+            setTimeout(() => resolve(InnerComp as any), 0),
+          ),
+      )
+
+      const include = ref('InnerComp')
+      const toggle = ref(true)
+      let cache: Map<any, any>
+
+      const App = defineVaporComponent({
+        setup() {
+          const ka = createComponent(
+            VaporKeepAlive,
+            { include: () => include.value },
+            {
+              default: () =>
+                createIf(
+                  () => toggle.value,
+                  () => createComponent(AsyncComp as any),
+                ),
+            },
+          )
+          cache = (ka as any).__v_cache
+          return ka
+        },
+      })
+
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+      const app = createVaporApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(container)
+
+      // wait for async component to resolve
+      await timeout()
+      await nextTick()
+      await nextTick()
+
+      expect(container.innerHTML).toContain('async inner')
+
+      // deactivate — should be cached since resolved name matches include
+      toggle.value = false
+      await nextTick()
+      expect(cache!.size).toBe(1)
+
+      // change include — resolved name still matches
+      include.value = 'InnerComp'
+      await nextTick()
+      expect(cache!.size).toBe(1)
+
+      // change include to exclude — should prune by resolved name
+      include.value = 'OtherComp'
+      await nextTick()
+      expect(cache!.size).toBe(0)
+    })
+
+    test('should not crash when toggling off interop async before resolve', async () => {
+      const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
+
+      let resolve: (comp: any) => void
+      const AsyncComp = defineAsyncComponent(
+        () =>
+          new Promise(r => {
+            resolve = r
+          }),
+      )
+
+      const InnerComp = {
+        name: 'InnerComp',
+        setup() {
+          return () => h('div', 'async inner')
+        },
+      }
+
+      const include = ref('InnerComp')
+      const toggle = ref(true)
+
+      const App = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VaporKeepAlive,
+            { include: () => include.value },
+            {
+              default: () =>
+                createIf(
+                  () => toggle.value,
+                  () => createComponent(AsyncComp as any),
+                ),
+            },
+          )
+        },
+      })
+
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+      const app = createVaporApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(container)
+
+      // toggle off BEFORE async resolves
+      toggle.value = false
+      await nextTick()
+
+      // resolve async component while toggled off
+      resolve!(InnerComp)
+      await timeout()
+      await nextTick()
+
+      // toggle back on — should remount fresh (not cached since was unresolved)
+      toggle.value = true
+      await nextTick()
+      await timeout()
+      await nextTick()
+
+      expect(container.innerHTML).toContain('async inner')
+    })
   })
 
   test('should invalidate pending mount/activated hooks when deactivated before post flush', async () => {

+ 5 - 2
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -424,7 +424,9 @@ const shouldCache = (
   props: KeepAliveProps,
   interop: boolean = false,
 ) => {
-  const isAsync = !interop && isAsyncWrapper(block as GenericComponentInstance)
+  const isAsync = isAsyncWrapper(
+    interop ? block.vnode! : (block as GenericComponentInstance),
+  )
   const type = (
     interop && isInteropEnabled
       ? (block as VaporFragment).vnode!.type
@@ -432,7 +434,8 @@ const shouldCache = (
   ) as GenericComponent & AsyncComponentInternalOptions
 
   // for unresolved async components, don't cache yet
-  // caching will be handled by AsyncWrapper calling keepAliveCtx.cacheBlock()
+  // - vapor async: caching deferred via keepAliveCtx.cacheBlock() in apiDefineAsyncComponent
+  // - vdom async: caching deferred via __asyncLoader().then() in createVDOMComponent
   if (isAsync && !type.__asyncResolved) {
     return false
   }

+ 7 - 0
packages/runtime-vapor/src/vdomInterop.ts

@@ -623,6 +623,13 @@ function createVDOMComponent(
 
   if (currentKeepAliveCtx) {
     currentKeepAliveCtx.processShapeFlag(frag)
+    // for VDOM async components, trigger cacheBlock after resolution
+    if ((component as any).__asyncLoader) {
+      const keepAliveCtx = currentKeepAliveCtx
+      ;(component as any).__asyncLoader().then(() => {
+        keepAliveCtx.cacheBlock()
+      })
+    }
     setCurrentKeepAliveCtx(null)
   }