]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): add support for async components in VaporKeepAlive (#14040)
authoredison <daiwei521@126.com>
Wed, 5 Nov 2025 03:55:57 +0000 (11:55 +0800)
committerGitHub <noreply@github.com>
Wed, 5 Nov 2025 03:55:57 +0000 (11:55 +0800)
packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts
packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
packages/runtime-vapor/src/apiDefineAsyncComponent.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/KeepAlive.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/vdomInterop.ts

index fa7f481707c776bbf27704f90dff09347969d394..7c0fb07ae5ef1b7d0b1ae1c39606f88533c3a15f 100644 (file)
@@ -1,10 +1,12 @@
-import { nextTick, ref } from '@vue/runtime-dom'
+import { nextTick, onActivated, ref } from '@vue/runtime-dom'
 import { type VaporComponent, createComponent } from '../src/component'
 import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
 import { makeRender } from './_utils'
 import {
+  VaporKeepAlive,
   createIf,
   createTemplateRefSetter,
+  defineVaporComponent,
   renderEffect,
   template,
 } from '@vue/runtime-vapor'
@@ -758,7 +760,102 @@ describe('api: defineAsyncComponent', () => {
 
   test.todo('suspense with error handling', async () => {})
 
-  test.todo('with KeepAlive', async () => {})
+  test('with KeepAlive', async () => {
+    const spy = vi.fn()
+    let resolve: (comp: VaporComponent) => void
+
+    const Foo = defineVaporAsyncComponent(
+      () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+    )
+
+    const Bar = defineVaporAsyncComponent(() =>
+      Promise.resolve(
+        defineVaporComponent({
+          setup() {
+            return template('Bar')()
+          },
+        }),
+      ),
+    )
+
+    const toggle = ref(true)
+    const { html } = define({
+      setup() {
+        return createComponent(VaporKeepAlive, null, {
+          default: () =>
+            createIf(
+              () => toggle.value,
+              () => createComponent(Foo),
+              () => createComponent(Bar),
+            ),
+        })
+      },
+    }).render()
+    expect(html()).toBe('<!--async component--><!--if-->')
+
+    await nextTick()
+    resolve!(
+      defineVaporComponent({
+        setup() {
+          onActivated(() => {
+            spy()
+          })
+          return template('Foo')()
+        },
+      }),
+    )
+
+    await timeout()
+    expect(html()).toBe('Foo<!--async component--><!--if-->')
+    expect(spy).toBeCalledTimes(1)
 
-  test.todo('with KeepAlive + include', async () => {})
+    toggle.value = false
+    await timeout()
+    expect(html()).toBe('Bar<!--async component--><!--if-->')
+  })
+
+  test('with KeepAlive + include', async () => {
+    const spy = vi.fn()
+    let resolve: (comp: VaporComponent) => void
+
+    const Foo = defineVaporAsyncComponent(
+      () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+    )
+
+    const { html } = define({
+      setup() {
+        return createComponent(
+          VaporKeepAlive,
+          { include: () => 'Foo' },
+          {
+            default: () => createComponent(Foo),
+          },
+        )
+      },
+    }).render()
+    expect(html()).toBe('<!--async component-->')
+
+    await nextTick()
+    resolve!(
+      defineVaporComponent({
+        name: 'Foo',
+        setup() {
+          onActivated(() => {
+            spy()
+          })
+          return template('Foo')()
+        },
+      }),
+    )
+
+    await timeout()
+    expect(html()).toBe('Foo<!--async component-->')
+    expect(spy).toBeCalledTimes(1)
+  })
 })
index 53d47651bbbed166f56b00361f532d6397fad725..09c4cbfc0c8260c31c1114b5beacad7eed594872 100644 (file)
@@ -22,6 +22,7 @@ import {
   createIf,
   createTemplateRefSetter,
   createVaporApp,
+  defineVaporAsyncComponent,
   defineVaporComponent,
   renderEffect,
   setText,
@@ -30,6 +31,7 @@ import {
 } from '../../src'
 
 const define = makeRender()
+const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
 
 describe('VaporKeepAlive', () => {
   let one: VaporComponent
@@ -1045,7 +1047,81 @@ describe('VaporKeepAlive', () => {
     })
   })
 
-  test.todo('should work with async component', async () => {})
+  test('should work with async component', async () => {
+    let resolve: (comp: VaporComponent) => void
+    const AsyncComp = defineVaporAsyncComponent(
+      () =>
+        new Promise(r => {
+          resolve = r as any
+        }),
+    )
+
+    const toggle = ref(true)
+    const instanceRef = ref<any>(null)
+    const { html } = define({
+      setup() {
+        const setRef = createTemplateRefSetter()
+        return createComponent(
+          VaporKeepAlive,
+          { include: () => 'Foo' },
+          {
+            default: () => {
+              return createIf(
+                () => toggle.value,
+                () => {
+                  const n0 = createComponent(AsyncComp)
+                  setRef(n0, instanceRef)
+                  return n0
+                },
+              )
+            },
+          },
+        )
+      },
+    }).render()
+
+    expect(html()).toBe(`<!--async component--><!--if-->`)
+
+    resolve!(
+      defineVaporComponent({
+        name: 'Foo',
+        setup(_, { expose }) {
+          const count = ref(0)
+          expose({
+            inc: () => {
+              count.value++
+            },
+          })
+
+          const n0 = template(`<p> </p>`)() as any
+          const x0 = child(n0) as any
+          renderEffect(() => {
+            setText(x0, String(count.value))
+          })
+          return n0
+        },
+      }),
+    )
+
+    await timeout()
+    // resolved
+    expect(html()).toBe(`<p>0</p><!--async component--><!--if-->`)
+
+    // change state + toggle out
+    instanceRef.value.inc()
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe('<!--if-->')
+
+    // toggle in, state should be maintained
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe('<p>1</p><!--async component--><!--if-->')
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe('<!--if-->')
+  })
 
   test('handle error in async onActivated', async () => {
     const err = new Error('foo')
@@ -1193,7 +1269,39 @@ describe('VaporKeepAlive', () => {
   })
 
   describe('vdom interop', () => {
-    test('render vdom component', async () => {
+    test('should work', () => {
+      const VdomComp = {
+        setup() {
+          onBeforeMount(() => oneHooks.beforeMount())
+          onMounted(() => oneHooks.mounted())
+          onActivated(() => oneHooks.activated())
+          onDeactivated(() => oneHooks.deactivated())
+          onUnmounted(() => oneHooks.unmounted())
+          return () => h('div', null, 'hi')
+        },
+      }
+
+      const App = defineVaporComponent({
+        setup() {
+          return createComponent(VaporKeepAlive, null, {
+            default: () => {
+              return createComponent(VdomComp)
+            },
+          })
+        },
+      })
+
+      const container = document.createElement('div')
+      document.body.appendChild(container)
+      const app = createVaporApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(container)
+
+      expect(container.innerHTML).toBe(`<div>hi</div>`)
+      assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+    })
+
+    test('with v-if', async () => {
       const VdomComp = {
         setup() {
           const msg = ref('vdom')
index 4cde9454f0953e120739ee58c4058cfae1a19af5..fcc706888a94f0c188f45beed687593421861bce 100644 (file)
@@ -5,6 +5,7 @@ import {
   createAsyncComponentContext,
   currentInstance,
   handleError,
+  isKeepAlive,
   markAsyncBoundary,
   performAsyncHydrate,
   useAsyncComponentState,
@@ -28,6 +29,7 @@ import {
 import { invokeArrayFns } from '@vue/shared'
 import { type TransitionOptions, insert, remove } from './block'
 import { parentNode } from './dom/node'
+import type { KeepAliveInstance } from './components/KeepAlive'
 import { setTransitionHooks } from './components/Transition'
 
 /*@ __NO_SIDE_EFFECTS__ */
@@ -122,7 +124,7 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
       // already resolved
       let resolvedComp = getResolvedComp()
       if (resolvedComp) {
-        frag!.update(() => createInnerComp(resolvedComp!, instance))
+        frag!.update(() => createInnerComp(resolvedComp!, instance, frag))
         return frag
       }
 
@@ -149,8 +151,6 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
       load()
         .then(() => {
           loaded.value = true
-          // TODO parent is keep-alive, force update so the loaded component's
-          // name is taken into account
         })
         .catch(err => {
           onError(err)
@@ -193,6 +193,14 @@ function createInnerComp(
     appContext,
   )
 
+  if (parent.parent && isKeepAlive(parent.parent)) {
+    // If there is a parent KeepAlive, let it handle the resolved async component
+    // This will process shapeFlag and cache the component
+    ;(parent.parent as KeepAliveInstance).cacheComponent(instance)
+    // cache the wrapper instance as well
+    ;(parent.parent as KeepAliveInstance).cacheComponent(parent)
+  }
+
   // set transition hooks
   if ($transition) setTransitionHooks(instance, $transition)
 
index 0f6d32ae8a53b2feba49b28072422e5d4e803676..628e6b61c7bd9d950107dc3e00f179916f44613a 100644 (file)
@@ -157,6 +157,11 @@ export function remove(block: Block, parent?: ParentNode): void {
     if (block.anchor) remove(block.anchor, parent)
     if ((block as DynamicFragment).scope) {
       ;(block as DynamicFragment).scope!.stop()
+      const scopes = (block as DynamicFragment).keptAliveScopes
+      if (scopes) {
+        scopes.forEach(scope => scope.stop())
+        scopes.clear()
+      }
     }
   }
 }
index c2c2cd1f0c2f59f3fe4630a2319c064d448a35e0..e850f08932df66f8cdc3683c18c2f17da32b51d7 100644 (file)
@@ -86,7 +86,10 @@ import {
 } from './dom/hydration'
 import { _next, createElement } from './dom/node'
 import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
-import type { KeepAliveInstance } from './components/KeepAlive'
+import {
+  type KeepAliveInstance,
+  findParentKeepAlive,
+} from './components/KeepAlive'
 import {
   insertionAnchor,
   insertionParent,
@@ -688,7 +691,7 @@ export function mountComponent(
   anchor?: Node | null | 0,
 ): void {
   if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
-    ;(instance.parent as KeepAliveInstance).activate(instance, parent, anchor)
+    findParentKeepAlive(instance)!.activate(instance, parent, anchor)
     return
   }
 
@@ -723,7 +726,7 @@ export function unmountComponent(
     instance.parent.vapor &&
     instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
   ) {
-    ;(instance.parent as KeepAliveInstance).deactivate(instance)
+    findParentKeepAlive(instance)!.deactivate(instance)
     return
   }
 
index c6ff01000d8dafe997b5b16bfb85899288e708f5..573fe613b96162ace2a30bc03a4c0f51c183cde0 100644 (file)
@@ -1,8 +1,12 @@
 import {
+  type GenericComponentInstance,
   type KeepAliveProps,
+  type VNode,
   currentInstance,
   devtoolsComponentAdded,
   getComponentName,
+  isAsyncWrapper,
+  isKeepAlive,
   matches,
   onBeforeUnmount,
   onMounted,
@@ -22,7 +26,11 @@ import {
 import { defineVaporComponent } from '../apiDefineComponent'
 import { ShapeFlags, invokeArrayFns, isArray } from '@vue/shared'
 import { createElement } from '../dom/node'
-import { type VaporFragment, isFragment } from '../fragment'
+import {
+  type DynamicFragment,
+  type VaporFragment,
+  isFragment,
+} from '../fragment'
 
 export interface KeepAliveInstance extends VaporComponentInstance {
   activate: (
@@ -31,14 +39,16 @@ export interface KeepAliveInstance extends VaporComponentInstance {
     anchor?: Node | null | 0,
   ) => void
   deactivate: (instance: VaporComponentInstance) => void
-  process: (block: Block) => void
+  cacheComponent: (instance: VaporComponentInstance) => void
   getCachedComponent: (
     comp: VaporComponent,
   ) => VaporComponentInstance | VaporFragment | undefined
   getStorageContainer: () => ParentNode
+  processFragment: (fragment: DynamicFragment) => void
+  cacheFragment: (fragment: DynamicFragment) => void
 }
 
-type CacheKey = VaporComponent
+type CacheKey = VaporComponent | VNode['type']
 type Cache = Map<CacheKey, VaporComponentInstance | VaporFragment>
 type Keys = Set<CacheKey>
 
@@ -66,22 +76,30 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
     }
 
     function shouldCache(instance: VaporComponentInstance) {
+      // For unresolved async wrappers, skip caching
+      // Wait for resolution and re-process in createInnerComp
+      if (isAsyncWrapper(instance) && !instance.type.__asyncResolved) {
+        return false
+      }
+
       const { include, exclude } = props
-      const name = getComponentName(instance.type)
+      const name = getComponentName(
+        isAsyncWrapper(instance)
+          ? instance.type.__asyncResolved!
+          : instance.type,
+      )
       return !(
         (include && (!name || !matches(include, name))) ||
         (exclude && name && matches(exclude, name))
       )
     }
 
-    function cacheBlock() {
+    function innerCacheBlock(
+      key: CacheKey,
+      instance: VaporComponentInstance | VaporFragment,
+    ) {
       const { max } = props
-      // TODO suspense
-      const block = keepAliveInstance.block!
-      const innerBlock = getInnerBlock(block)!
-      if (!innerBlock || !shouldCache(innerBlock)) return
 
-      const key = innerBlock.type
       if (cache.has(key)) {
         // make this key the freshest
         keys.delete(key)
@@ -93,56 +111,116 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
           pruneCacheEntry(keys.values().next().value!)
         }
       }
-      cache.set(
-        key,
-        (current =
-          isFragment(block) && isFragment(block.nodes)
-            ? // cache the fragment nodes for vdom interop
-              block.nodes
-            : innerBlock),
-      )
+
+      cache.set(key, instance)
+      current = instance
+    }
+
+    function cacheBlock() {
+      // TODO suspense
+      const block = keepAliveInstance.block!
+      const innerBlock = getInnerBlock(block)!
+      if (!innerBlock || !shouldCache(innerBlock)) return
+
+      let toCache: VaporComponentInstance | VaporFragment
+      let key: CacheKey
+      let frag: VaporFragment | undefined
+      if (isFragment(block) && (frag = findInteropFragment(block))) {
+        // vdom component: cache the fragment
+        toCache = frag
+        key = frag.vnode!.type
+      } else {
+        // vapor component: cache the instance
+        toCache = innerBlock
+        key = innerBlock.type
+      }
+      innerCacheBlock(key, toCache)
     }
 
     onMounted(cacheBlock)
     onUpdated(cacheBlock)
 
     onBeforeUnmount(() => {
-      cache.forEach(item => {
-        const cached = getInnerComponent(item)!
-        resetShapeFlag(cached)
-        cache.delete(cached.type)
+      cache.forEach((cached, key) => {
+        const instance = getInstanceFromCache(cached)
+        if (!instance) return
+
+        resetCachedShapeFlag(cached)
+        cache.delete(key)
+
         // current instance will be unmounted as part of keep-alive's unmount
         if (current) {
-          const innerComp = getInnerComponent(current)!
-          if (innerComp.type === cached.type) {
-            const instance = cached.vapor
-              ? cached
-              : // vdom interop
-                (cached as any).component
+          const currentKey = isVaporComponent(current)
+            ? current.type
+            : current.vnode!.type
+          if (currentKey === key) {
+            // call deactivated hook
             const da = instance.da
             da && queuePostFlushCb(da)
             return
           }
         }
-        remove(item, storageContainer)
+
+        remove(cached, storageContainer)
       })
     })
 
     keepAliveInstance.getStorageContainer = () => storageContainer
-    keepAliveInstance.getCachedComponent = comp => cache.get(comp)
 
-    const processShapeFlag = (keepAliveInstance.process = block => {
-      const instance = getInnerComponent(block)
-      if (!instance) return
+    keepAliveInstance.getCachedComponent = comp => {
+      return cache.get(comp)
+    }
+
+    keepAliveInstance.cacheComponent = (instance: VaporComponentInstance) => {
+      if (!shouldCache(instance)) return
+      instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+      innerCacheBlock(instance.type, instance)
+    }
+
+    keepAliveInstance.processFragment = (frag: DynamicFragment) => {
+      const innerBlock = getInnerBlock(frag.nodes)
+      if (!innerBlock) return
 
-      if (cache.has(instance.type)) {
-        instance.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+      const fragment = findInteropFragment(frag.nodes)
+      if (fragment) {
+        if (cache.has(fragment.vnode!.type)) {
+          fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+        }
+        if (shouldCache(innerBlock)) {
+          fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        }
+      } else {
+        if (cache.has(innerBlock.type)) {
+          innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+        }
+        if (shouldCache(innerBlock)) {
+          innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        }
       }
+    }
+
+    keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => {
+      const innerBlock = getInnerBlock(fragment.nodes)
+      if (!innerBlock || !shouldCache(innerBlock)) return
 
-      if (shouldCache(instance)) {
-        instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+      // Determine what to cache based on fragment type
+      let toCache: VaporComponentInstance | VaporFragment
+      let key: CacheKey
+
+      // find vdom interop fragment
+      const frag = findInteropFragment(fragment)
+      if (frag) {
+        frag.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        toCache = frag
+        key = frag.vnode!.type
+      } else {
+        innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        toCache = innerBlock
+        key = innerBlock.type
       }
-    })
+
+      innerCacheBlock(key, toCache)
+    }
 
     keepAliveInstance.activate = (instance, parentNode, anchor) => {
       current = instance
@@ -154,6 +232,16 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       deactivate(instance, storageContainer)
     }
 
+    function resetCachedShapeFlag(
+      cached: VaporComponentInstance | VaporFragment,
+    ) {
+      if (isVaporComponent(cached)) {
+        resetShapeFlag(cached)
+      } else {
+        resetShapeFlag(cached.vnode)
+      }
+    }
+
     let children = slots.default()
     if (isArray(children) && children.length > 1) {
       if (__DEV__) {
@@ -162,15 +250,18 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       return children
     }
 
-    // `children` could be either a `VaporComponentInstance` or a `DynamicFragment`
-    // (when using `v-if` or `<component is/>`). For `DynamicFragment` children,
-    // the `shapeFlag` is processed in `DynamicFragment.update`. Here only need
-    // to process the `VaporComponentInstance`
-    if (isVaporComponent(children)) processShapeFlag(children)
+    // Process shapeFlag for vapor and vdom components
+    // DynamicFragment (v-if, <component is/>) is processed in DynamicFragment.update
+    if (isVaporComponent(children)) {
+      children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+    } else if (isInteropFragment(children)) {
+      children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+    }
 
     function pruneCache(filter: (name: string) => boolean) {
-      cache.forEach((instance, key) => {
-        instance = getInnerComponent(instance)!
+      cache.forEach((cached, key) => {
+        const instance = getInstanceFromCache(cached)
+        if (!instance) return
         const name = getComponentName(instance.type)
         if (name && !filter(name)) {
           pruneCacheEntry(key)
@@ -180,7 +271,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
 
     function pruneCacheEntry(key: CacheKey) {
       const cached = cache.get(key)!
-      resetShapeFlag(cached)
+
+      resetCachedShapeFlag(cached)
+
       // don't unmount if the instance is the current one
       if (cached !== current) {
         remove(cached)
@@ -207,26 +300,34 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
 function getInnerBlock(block: Block): VaporComponentInstance | undefined {
   if (isVaporComponent(block)) {
     return block
-  }
-  if (isVdomInteropFragment(block)) {
+  } else if (isInteropFragment(block)) {
     return block.vnode as any
-  }
-  if (isFragment(block)) {
+  } else if (isFragment(block)) {
     return getInnerBlock(block.nodes)
   }
 }
 
-function getInnerComponent(block: Block): VaporComponentInstance | undefined {
-  if (isVaporComponent(block)) {
+function isInteropFragment(block: Block): block is VaporFragment {
+  return !!(isFragment(block) && block.vnode)
+}
+
+function findInteropFragment(block: Block): VaporFragment | undefined {
+  if (isInteropFragment(block)) {
     return block
-  } else if (isVdomInteropFragment(block)) {
-    // vdom interop
-    return block.vnode as any
+  }
+  if (isFragment(block)) {
+    return findInteropFragment(block.nodes)
   }
 }
 
-function isVdomInteropFragment(block: Block): block is VaporFragment {
-  return !!(isFragment(block) && block.insert)
+function getInstanceFromCache(
+  cached: VaporComponentInstance | VaporFragment,
+): GenericComponentInstance {
+  if (isVaporComponent(cached)) {
+    return cached
+  }
+  // vdom interop
+  return cached.vnode!.component as GenericComponentInstance
 }
 
 export function activate(
@@ -261,3 +362,16 @@ 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
+}
index 07f1243e4e5076acf4b4cba41af9c5d9f8eb9440..e30909ea067b564a01d9b79a9732a2bac479ea5e 100644 (file)
@@ -73,6 +73,8 @@ export class DynamicFragment extends VaporFragment {
   current?: BlockFn
   fallback?: BlockFn
   anchorLabel?: string
+  inKeepAlive?: boolean
+  keptAliveScopes?: Map<any, EffectScope>
 
   constructor(anchorLabel?: string) {
     super([])
@@ -96,11 +98,13 @@ export class DynamicFragment extends VaporFragment {
     const parent = isHydrating ? null : this.anchor.parentNode
     const transition = this.$transition
     const instance = currentInstance!
-
+    this.inKeepAlive = isKeepAlive(instance)
     // teardown previous branch
     if (this.scope) {
-      if (isKeepAlive(instance)) {
-        ;(instance as KeepAliveInstance).process(this.nodes)
+      if (this.inKeepAlive) {
+        ;(instance as KeepAliveInstance).processFragment(this)
+        if (!this.keptAliveScopes) this.keptAliveScopes = new Map()
+        this.keptAliveScopes.set(this.current, this.scope)
       } else {
         this.scope.stop()
       }
@@ -156,10 +160,22 @@ export class DynamicFragment extends VaporFragment {
     parent: ParentNode | null,
   ) {
     if (render) {
-      this.scope = new EffectScope()
+      // For KeepAlive, try to reuse the keepAlive scope for this key
+      const scope =
+        this.inKeepAlive && this.keptAliveScopes
+          ? this.keptAliveScopes.get(this.current)
+          : undefined
+      if (scope) {
+        this.scope = scope
+        this.keptAliveScopes!.delete(this.current!)
+        this.scope.resume()
+      } else {
+        this.scope = new EffectScope()
+      }
+
       this.nodes = this.scope.run(render) || []
-      if (isKeepAlive(instance)) {
-        ;(instance as KeepAliveInstance).process(this.nodes)
+      if (this.inKeepAlive) {
+        ;(instance as KeepAliveInstance).cacheFragment(this)
       }
       if (transition) {
         this.$transition = applyTransitionHooks(this.nodes, transition)
index a44c078e6aacab6d768f86ec1bd2e397f1b14474..c516f64c17aed095002974c96b582e37da40f99d 100644 (file)
@@ -76,9 +76,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'
 
 export const interopKey: unique symbol = Symbol(`interop`)
@@ -276,6 +276,7 @@ function createVDOMComponent(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
 ): VaporFragment {
+  const parentInstance = currentInstance as VaporComponentInstance
   const frag = new VaporFragment([])
   const vnode = (frag.vnode = createVNode(
     component,
@@ -307,7 +308,6 @@ function createVDOMComponent(
 
   let rawRef: VNodeNormalizedRef | null = null
   let isMounted = false
-  const parentInstance = currentInstance as VaporComponentInstance
   const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
     // unset ref
     if (rawRef) vdomSetRef(rawRef, null, null, vnode, true)
@@ -315,7 +315,7 @@ function createVDOMComponent(
     if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
       vdomDeactivate(
         vnode,
-        (parentInstance as KeepAliveInstance).getStorageContainer(),
+        findParentKeepAlive(parentInstance)!.getStorageContainer(),
         internals,
         parentInstance as any,
         null,
@@ -348,39 +348,38 @@ function createVDOMComponent(
         undefined,
         false,
       )
-      return
-    }
-
-    const prev = currentInstance
-    simpleSetCurrentInstance(parentInstance)
-    if (!isMounted) {
-      if (transition) setVNodeTransitionHooks(vnode, transition)
-      internals.mt(
-        vnode,
-        parentNode,
-        anchor,
-        parentInstance as any,
-        null,
-        undefined,
-        false,
-      )
-      // set ref
-      if (rawRef) vdomSetRef(rawRef, null, null, vnode)
-      onScopeDispose(unmount, true)
-      isMounted = true
     } else {
-      // move
-      internals.m(
-        vnode,
-        parentNode,
-        anchor,
-        MoveType.REORDER,
-        parentInstance as any,
-      )
+      const prev = currentInstance
+      simpleSetCurrentInstance(parentInstance)
+      if (!isMounted) {
+        if (transition) setVNodeTransitionHooks(vnode, transition)
+        internals.mt(
+          vnode,
+          parentNode,
+          anchor,
+          parentInstance as any,
+          null,
+          undefined,
+          false,
+        )
+        // set ref
+        if (rawRef) vdomSetRef(rawRef, null, null, vnode)
+        onScopeDispose(unmount, true)
+        isMounted = true
+      } else {
+        // move
+        internals.m(
+          vnode,
+          parentNode,
+          anchor,
+          MoveType.REORDER,
+          parentInstance as any,
+        )
+      }
+      simpleSetCurrentInstance(prev)
     }
 
     frag.nodes = vnode.el as any
-    simpleSetCurrentInstance(prev)
   }
 
   frag.remove = unmount