Bladeren bron

fix(keep-alive): improve KeepAlive caching behavior for async components by re-evaluating caching after resolution (#14285)

edison 3 maanden geleden
bovenliggende
commit
6fc638ffa8

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

@@ -1123,6 +1123,76 @@ describe('VaporKeepAlive', () => {
     expect(html()).toBe('<!--if-->')
   })
 
+  test('should not cache async component when resolved name does not match include', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const AsyncComp = defineVaporAsyncComponent(
+      () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+    )
+
+    const mounted = vi.fn()
+    const unmounted = vi.fn()
+    const activated = vi.fn()
+    const deactivated = vi.fn()
+
+    const toggle = ref(true)
+    const { html } = define({
+      setup() {
+        return createComponent(
+          VaporKeepAlive,
+          // include only 'SomeOtherName', not 'Bar'
+          { include: () => 'SomeOtherName' },
+          {
+            default: () => {
+              return createIf(
+                () => toggle.value,
+                () => createComponent(AsyncComp),
+              )
+            },
+          },
+        )
+      },
+    }).render()
+
+    expect(html()).toBe(`<!--async component--><!--if-->`)
+
+    // Resolve with name 'Bar' which doesn't match include 'SomeOtherName'
+    resolve!(
+      defineVaporComponent({
+        name: 'Bar',
+        setup() {
+          onMounted(mounted)
+          onUnmounted(unmounted)
+          onActivated(activated)
+          onDeactivated(deactivated)
+          return template(`<div>Bar</div>`)()
+        },
+      }),
+    )
+
+    await timeout()
+    expect(html()).toBe(`<div>Bar</div><!--async component--><!--if-->`)
+    expect(mounted).toHaveBeenCalledTimes(1)
+    // Should NOT call activated because it doesn't match include
+    expect(activated).toHaveBeenCalledTimes(0)
+
+    // Toggle off - should unmount, NOT deactivate (because not cached)
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe('<!--if-->')
+    expect(unmounted).toHaveBeenCalledTimes(1)
+    expect(deactivated).toHaveBeenCalledTimes(0)
+
+    // Toggle on - should remount, NOT activate from cache
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>Bar</div><!--async component--><!--if-->`)
+    expect(mounted).toHaveBeenCalledTimes(2) // Should be called again
+    expect(activated).toHaveBeenCalledTimes(0)
+  })
+
   test('handle error in async onActivated', async () => {
     const err = new Error('foo')
     const handler = vi.fn()

+ 6 - 0
packages/runtime-vapor/src/apiDefineAsyncComponent.ts

@@ -5,6 +5,7 @@ import {
   createAsyncComponentContext,
   currentInstance,
   handleError,
+  isKeepAlive,
   markAsyncBoundary,
   performAsyncHydrate,
   useAsyncComponentState,
@@ -29,6 +30,7 @@ import { invokeArrayFns } from '@vue/shared'
 import { type TransitionOptions, insert, remove } from './block'
 import { parentNode } from './dom/node'
 import { setTransitionHooks } from './components/Transition'
+import type { KeepAliveInstance } from './components/KeepAlive'
 
 /*@ __NO_SIDE_EFFECTS__ */
 export function defineVaporAsyncComponent<T extends VaporComponent>(
@@ -149,6 +151,10 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
       load()
         .then(() => {
           loaded.value = true
+          // if parent is keep-alive, re-evaluate caching
+          if (instance.parent && isKeepAlive(instance.parent)) {
+            ;(instance.parent as KeepAliveInstance).ctx.onAsyncResolve(instance)
+          }
         })
         .catch(err => {
           onError(err)

+ 10 - 9
packages/runtime-vapor/src/component.ts

@@ -105,10 +105,7 @@ import {
   isTeleportFragment,
   isVaporTeleport,
 } from './components/Teleport'
-import {
-  type KeepAliveInstance,
-  findParentKeepAlive,
-} from './components/KeepAlive'
+import type { KeepAliveInstance } from './components/KeepAlive'
 import {
   insertionAnchor,
   insertionParent,
@@ -281,9 +278,9 @@ export function createComponent(
     currentInstance.vapor &&
     isKeepAlive(currentInstance)
   ) {
-    const cached = (currentInstance as KeepAliveInstance).getCachedComponent(
-      component,
-    )
+    const cached = (
+      currentInstance as KeepAliveInstance
+    ).ctx.getCachedComponent(component)
     // @ts-expect-error
     if (cached) return cached
   }
@@ -873,7 +870,11 @@ export function mountComponent(
   }
 
   if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
-    findParentKeepAlive(instance)!.activate(instance, parent, anchor)
+    ;(instance.parent as KeepAliveInstance)!.ctx.activate(
+      instance,
+      parent,
+      anchor,
+    )
     return
   }
 
@@ -919,7 +920,7 @@ export function unmountComponent(
     instance.parent.vapor &&
     instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
   ) {
-    findParentKeepAlive(instance)!.deactivate(instance)
+    ;(instance.parent as KeepAliveInstance)!.ctx.deactivate(instance)
     return
   }
 

+ 34 - 38
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -9,7 +9,6 @@ import {
   devtoolsComponentAdded,
   getComponentName,
   isAsyncWrapper,
-  isKeepAlive,
   matches,
   onBeforeUnmount,
   onMounted,
@@ -38,16 +37,19 @@ import {
 import type { EffectScope } from '@vue/reactivity'
 
 export interface KeepAliveInstance extends VaporComponentInstance {
-  activate: (
-    instance: VaporComponentInstance,
-    parentNode: ParentNode,
-    anchor?: Node | null | 0,
-  ) => void
-  deactivate: (instance: VaporComponentInstance) => void
-  getCachedComponent: (
-    comp: VaporComponent,
-  ) => VaporComponentInstance | VaporFragment | undefined
-  getStorageContainer: () => ParentNode
+  ctx: {
+    activate: (
+      instance: VaporComponentInstance,
+      parentNode: ParentNode,
+      anchor?: Node | null | 0,
+    ) => void
+    deactivate: (instance: VaporComponentInstance) => void
+    getCachedComponent: (
+      comp: VaporComponent,
+    ) => VaporComponentInstance | VaporFragment | undefined
+    getStorageContainer: () => ParentNode
+    onAsyncResolve: (asyncWrapper: VaporComponentInstance) => void
+  }
 }
 
 type CacheKey = VaporComponent | VNode['type']
@@ -78,18 +80,24 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       ;(keepAliveInstance as any).__v_cache = cache
     }
 
-    keepAliveInstance.getStorageContainer = () => storageContainer
-
-    keepAliveInstance.getCachedComponent = comp => cache.get(comp)
-
-    keepAliveInstance.activate = (instance, parentNode, anchor) => {
-      current = instance
-      activate(instance, parentNode, anchor)
-    }
-
-    keepAliveInstance.deactivate = instance => {
-      current = undefined
-      deactivate(instance, storageContainer)
+    keepAliveInstance.ctx = {
+      getStorageContainer: () => storageContainer,
+      getCachedComponent: comp => cache.get(comp),
+      activate: (instance, parentNode, anchor) => {
+        current = instance
+        activate(instance, parentNode, anchor)
+      },
+      deactivate: instance => {
+        current = undefined
+        deactivate(instance, storageContainer)
+      },
+      // called when async component resolves to evaluate caching
+      onAsyncResolve: (asyncWrapper: VaporComponentInstance) => {
+        if (shouldCache(asyncWrapper, props, false)) {
+          asyncWrapper.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+          innerCacheBlock(asyncWrapper.type, asyncWrapper)
+        }
+      },
     }
 
     const innerCacheBlock = (
@@ -293,9 +301,10 @@ const shouldCache = (
       : (block as GenericComponentInstance).type
   ) as GenericComponent & AsyncComponentInternalOptions
 
-  // return true to ensure hooks are injected into its block (DynamicFragment)
+  // for unresolved async components, don't cache yet
+  // caching will be done in onAsyncResolve after the component resolves
   if (isAsync && !type.__asyncResolved) {
-    return true
+    return false
   }
 
   const { include, exclude } = props
@@ -378,16 +387,3 @@ export function deactivate(
     devtoolsComponentAdded(instance)
   }
 }
-
-export function findParentKeepAlive(
-  instance: VaporComponentInstance,
-): KeepAliveInstance | null {
-  let parent = instance as GenericComponentInstance | null
-  while (parent) {
-    if (isKeepAlive(parent)) {
-      return parent as KeepAliveInstance
-    }
-    parent = parent.parent
-  }
-  return null
-}

+ 2 - 2
packages/runtime-vapor/src/vdomInterop.ts

@@ -78,9 +78,9 @@ import { VaporFragment, isFragment, setFragmentFallback } from './fragment'
 import type { NodeRef } from './apiTemplateRef'
 import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
 import {
+  type KeepAliveInstance,
   activate,
   deactivate,
-  findParentKeepAlive,
 } from './components/KeepAlive'
 import { setParentSuspense } from './components/Suspense'
 
@@ -344,7 +344,7 @@ function createVDOMComponent(
     if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
       vdomDeactivate(
         vnode,
-        findParentKeepAlive(parentComponent!)!.getStorageContainer(),
+        (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
         internals,
         parentComponent as any,
         null,