]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(keep-alive): refactor KeepAlive to use new fragment hooks. (#14121)
authoredison <daiwei521@126.com>
Thu, 20 Nov 2025 07:51:02 +0000 (15:51 +0800)
committerGitHub <noreply@github.com>
Thu, 20 Nov 2025 07:51:02 +0000 (15:51 +0800)
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts
packages/runtime-vapor/src/apiDefineAsyncComponent.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/components/KeepAlive.ts
packages/runtime-vapor/src/fragment.ts

index 67ff4f0e913fc5b5770d20e0909708bc8fa4830c..6c968905a6700cf7c818216062d3108934e9a1ab 100644 (file)
@@ -669,3 +669,8 @@ export {
   checkTransitionMode,
   leaveCbKey,
 } from './components/BaseTransition'
+
+/**
+ * @internal
+ */
+export type { GenericComponent } from './component'
index 7c0fb07ae5ef1b7d0b1ae1c39606f88533c3a15f..a6ada37df0ea610e52d278f8a2f344d6300c3fe6 100644 (file)
@@ -9,6 +9,7 @@ import {
   defineVaporComponent,
   renderEffect,
   template,
+  withVaporCtx,
 } from '@vue/runtime-vapor'
 import { setElementText } from '../src/dom/prop'
 
@@ -785,12 +786,13 @@ describe('api: defineAsyncComponent', () => {
     const { html } = define({
       setup() {
         return createComponent(VaporKeepAlive, null, {
-          default: () =>
+          default: withVaporCtx(() =>
             createIf(
               () => toggle.value,
               () => createComponent(Foo),
               () => createComponent(Bar),
             ),
+          ),
         })
       },
     }).render()
@@ -834,7 +836,7 @@ describe('api: defineAsyncComponent', () => {
           VaporKeepAlive,
           { include: () => 'Foo' },
           {
-            default: () => createComponent(Foo),
+            default: withVaporCtx(() => createComponent(Foo)),
           },
         )
       },
index 25b99c987f30e644a8ce68456bca1235b54cbe6c..1b7c82bbefc66f8d53512fa95ff7acbf878176b5 100644 (file)
@@ -5,7 +5,6 @@ import {
   createAsyncComponentContext,
   currentInstance,
   handleError,
-  isKeepAlive,
   markAsyncBoundary,
   performAsyncHydrate,
   useAsyncComponentState,
@@ -29,7 +28,6 @@ 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__ */
@@ -193,14 +191,6 @@ 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 27c9e333716f24b70917feec120392a51118f6e5..eb3a2bde2d6669f28525756b595a36b1025dc1bc 100644 (file)
@@ -160,11 +160,6 @@ 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 573fe613b96162ace2a30bc03a4c0f51c183cde0..2e285e6ab3a3792898455d323dcbb5088efcf3f8 100644 (file)
@@ -1,4 +1,6 @@
 import {
+  type AsyncComponentInternalOptions,
+  type GenericComponent,
   type GenericComponentInstance,
   type KeepAliveProps,
   type VNode,
@@ -29,8 +31,10 @@ import { createElement } from '../dom/node'
 import {
   type DynamicFragment,
   type VaporFragment,
+  isDynamicFragment,
   isFragment,
 } from '../fragment'
+import type { EffectScope } from '@vue/reactivity'
 
 export interface KeepAliveInstance extends VaporComponentInstance {
   activate: (
@@ -39,13 +43,10 @@ export interface KeepAliveInstance extends VaporComponentInstance {
     anchor?: Node | null | 0,
   ) => void
   deactivate: (instance: VaporComponentInstance) => void
-  cacheComponent: (instance: VaporComponentInstance) => void
   getCachedComponent: (
     comp: VaporComponent,
   ) => VaporComponentInstance | VaporFragment | undefined
   getStorageContainer: () => ParentNode
-  processFragment: (fragment: DynamicFragment) => void
-  cacheFragment: (fragment: DynamicFragment) => void
 }
 
 type CacheKey = VaporComponent | VNode['type']
@@ -69,35 +70,31 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
     const cache: Cache = new Map()
     const keys: Keys = new Set()
     const storageContainer = createElement('div')
+    const keptAliveScopes = new Map<any, EffectScope>()
     let current: VaporComponentInstance | VaporFragment | undefined
 
     if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
       ;(keepAliveInstance as any).__v_cache = cache
     }
 
-    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
-      }
+    keepAliveInstance.getStorageContainer = () => storageContainer
 
-      const { include, exclude } = props
-      const name = getComponentName(
-        isAsyncWrapper(instance)
-          ? instance.type.__asyncResolved!
-          : instance.type,
-      )
-      return !(
-        (include && (!name || !matches(include, name))) ||
-        (exclude && name && matches(exclude, name))
-      )
+    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)
     }
 
-    function innerCacheBlock(
+    const innerCacheBlock = (
       key: CacheKey,
       instance: VaporComponentInstance | VaporFragment,
-    ) {
+    ) => {
       const { max } = props
 
       if (cache.has(key)) {
@@ -116,149 +113,54 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       current = instance
     }
 
-    function cacheBlock() {
+    const 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((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 currentKey = isVaporComponent(current)
-            ? current.type
-            : current.vnode!.type
-          if (currentKey === key) {
-            // call deactivated hook
-            const da = instance.da
-            da && queuePostFlushCb(da)
-            return
-          }
-        }
-
-        remove(cached, storageContainer)
-      })
-    })
-
-    keepAliveInstance.getStorageContainer = () => storageContainer
-
-    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)
+      const [innerBlock, interop] = getInnerBlock(block)!
+      if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
+      innerCacheBlock(
+        interop ? innerBlock.vnode!.type : innerBlock.type,
+        innerBlock,
+      )
     }
 
-    keepAliveInstance.processFragment = (frag: DynamicFragment) => {
-      const innerBlock = getInnerBlock(frag.nodes)
-      if (!innerBlock) return
+    const processFragment = (frag: DynamicFragment) => {
+      const [innerBlock, interop] = getInnerBlock(frag.nodes)
+      if (!innerBlock && !shouldCache(innerBlock!, props, interop)) return
 
-      const fragment = findInteropFragment(frag.nodes)
-      if (fragment) {
-        if (cache.has(fragment.vnode!.type)) {
-          fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+      if (interop) {
+        if (cache.has(innerBlock.vnode!.type)) {
+          innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
         }
-        if (shouldCache(innerBlock)) {
-          fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        if (shouldCache(innerBlock!, props, true)) {
+          innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
         }
       } else {
-        if (cache.has(innerBlock.type)) {
-          innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
+        if (cache.has(innerBlock!.type)) {
+          innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
         }
-        if (shouldCache(innerBlock)) {
-          innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        if (shouldCache(innerBlock!, props)) {
+          innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
         }
       }
     }
 
-    keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => {
-      const innerBlock = getInnerBlock(fragment.nodes)
-      if (!innerBlock || !shouldCache(innerBlock)) return
+    const cacheFragment = (fragment: DynamicFragment) => {
+      const [innerBlock, interop] = getInnerBlock(fragment.nodes)
+      if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
 
-      // 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
+      if (interop) {
+        innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+        key = innerBlock.vnode!.type
       } else {
         innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
-        toCache = innerBlock
         key = innerBlock.type
       }
-
-      innerCacheBlock(key, toCache)
+      innerCacheBlock(key, innerBlock)
     }
 
-    keepAliveInstance.activate = (instance, parentNode, anchor) => {
-      current = instance
-      activate(instance, parentNode, anchor)
-    }
-
-    keepAliveInstance.deactivate = instance => {
-      current = undefined
-      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__) {
-        warn(`KeepAlive should contain exactly one component child.`)
-      }
-      return 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) {
+    const pruneCache = (filter: (name: string) => boolean) => {
       cache.forEach((cached, key) => {
         const instance = getInstanceFromCache(cached)
         if (!instance) return
@@ -269,7 +171,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       })
     }
 
-    function pruneCacheEntry(key: CacheKey) {
+    const pruneCacheEntry = (key: CacheKey) => {
       const cached = cache.get(key)!
 
       resetCachedShapeFlag(cached)
@@ -293,33 +195,142 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       { flush: 'post', deep: true },
     )
 
+    onMounted(cacheBlock)
+    onUpdated(cacheBlock)
+    onBeforeUnmount(() => {
+      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 currentKey = isVaporComponent(current)
+            ? current.type
+            : current.vnode!.type
+          if (currentKey === key) {
+            // call deactivated hook
+            const da = instance.da
+            da && queuePostFlushCb(da)
+            return
+          }
+        }
+
+        remove(cached, storageContainer)
+      })
+      keptAliveScopes.forEach(scope => scope.stop())
+      keptAliveScopes.clear()
+    })
+
+    let children = slots.default()
+    if (isArray(children)) {
+      children = children.filter(child => !(child instanceof Comment))
+      if (children.length > 1) {
+        if (__DEV__) {
+          warn(`KeepAlive should contain exactly one component child.`)
+        }
+        return children
+      }
+    }
+
+    // inject hooks to DynamicFragment to cache components during updates
+    const injectKeepAliveHooks = (frag: DynamicFragment) => {
+      ;(frag.beforeTeardown || (frag.beforeTeardown = [])).push(
+        (oldKey, nodes, scope) => {
+          processFragment(frag)
+          keptAliveScopes.set(oldKey, scope)
+          return true
+        },
+      )
+      ;(frag.beforeMount || (frag.beforeMount = [])).push(() =>
+        cacheFragment(frag),
+      )
+      frag.getScope = key => {
+        const scope = keptAliveScopes.get(key)
+        if (scope) {
+          keptAliveScopes.delete(key)
+          return scope
+        }
+      }
+    }
+
+    // process shapeFlag
+    if (isVaporComponent(children)) {
+      children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+      if (isAsyncWrapper(children)) {
+        injectKeepAliveHooks(children.block as DynamicFragment)
+      }
+    } else if (isInteropFragment(children)) {
+      children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+    } else if (isDynamicFragment(children)) {
+      processFragment(children)
+      injectKeepAliveHooks(children)
+      if (isVaporComponent(children.nodes) && isAsyncWrapper(children.nodes)) {
+        injectKeepAliveHooks(children.nodes.block as DynamicFragment)
+      }
+    }
+
     return children
   },
 })
 
-function getInnerBlock(block: Block): VaporComponentInstance | undefined {
+const shouldCache = (
+  block: GenericComponentInstance | VaporFragment,
+  props: KeepAliveProps,
+  interop: boolean = false,
+) => {
+  const isAsync = !interop && isAsyncWrapper(block as GenericComponentInstance)
+  const type = (
+    interop
+      ? (block as VaporFragment).vnode!.type
+      : (block as GenericComponentInstance).type
+  ) as GenericComponent & AsyncComponentInternalOptions
+
+  // return true to ensure hooks are injected into its block (DynamicFragment)
+  if (isAsync && !type.__asyncResolved) {
+    return true
+  }
+
+  const { include, exclude } = props
+  const name = getComponentName(isAsync ? type.__asyncResolved! : type)
+  return !(
+    (include && (!name || !matches(include, name))) ||
+    (exclude && name && matches(exclude, name))
+  )
+}
+
+const resetCachedShapeFlag = (
+  cached: VaporComponentInstance | VaporFragment,
+) => {
+  if (isVaporComponent(cached)) {
+    resetShapeFlag(cached)
+  } else {
+    resetShapeFlag(cached.vnode)
+  }
+}
+
+type InnerBlockResult =
+  | [VaporFragment, true]
+  | [VaporComponentInstance, false]
+  | [undefined, false]
+
+function getInnerBlock(block: Block): InnerBlockResult {
   if (isVaporComponent(block)) {
-    return block
+    return [block, false]
   } else if (isInteropFragment(block)) {
-    return block.vnode as any
+    return [block, true]
   } else if (isFragment(block)) {
     return getInnerBlock(block.nodes)
   }
+  return [undefined, false]
 }
 
 function isInteropFragment(block: Block): block is VaporFragment {
   return !!(isFragment(block) && block.vnode)
 }
 
-function findInteropFragment(block: Block): VaporFragment | undefined {
-  if (isInteropFragment(block)) {
-    return block
-  }
-  if (isFragment(block)) {
-    return findInteropFragment(block.nodes)
-  }
-}
-
 function getInstanceFromCache(
   cached: VaporComponentInstance | VaporFragment,
 ): GenericComponentInstance {
index e30909ea067b564a01d9b79a9732a2bac479ea5e..d6a0f8b6c659d20b1c3dad8ebdbebc0e26bbfdad 100644 (file)
@@ -11,16 +11,12 @@ import {
   remove,
 } from './block'
 import {
-  type GenericComponentInstance,
   type TransitionHooks,
   type VNode,
-  currentInstance,
-  isKeepAlive,
   queuePostFlushCb,
 } from '@vue/runtime-dom'
 import type { VaporComponentInstance } from './component'
 import type { NodeRef } from './apiTemplateRef'
-import type { KeepAliveInstance } from './components/KeepAlive'
 import {
   applyTransitionHooks,
   applyTransitionLeaveHooks,
@@ -73,8 +69,18 @@ export class DynamicFragment extends VaporFragment {
   current?: BlockFn
   fallback?: BlockFn
   anchorLabel?: string
-  inKeepAlive?: boolean
-  keptAliveScopes?: Map<any, EffectScope>
+
+  // get the kept-alive scope when used in keep-alive
+  getScope?: (key: any) => EffectScope | undefined
+
+  // hooks
+  beforeTeardown?: ((
+    oldKey: any,
+    nodes: Block,
+    scope: EffectScope,
+  ) => boolean)[]
+  beforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
+  mounted?: ((nodes: Block, scope: EffectScope) => void)[]
 
   constructor(anchorLabel?: string) {
     super([])
@@ -97,21 +103,23 @@ export class DynamicFragment extends VaporFragment {
     const prevSub = setActiveSub()
     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 (this.inKeepAlive) {
-        ;(instance as KeepAliveInstance).processFragment(this)
-        if (!this.keptAliveScopes) this.keptAliveScopes = new Map()
-        this.keptAliveScopes.set(this.current, this.scope)
-      } else {
+      let preserveScope = false
+      // if any of the hooks returns true the scope will be preserved
+      // for kept-alive component
+      if (this.beforeTeardown) {
+        preserveScope = this.beforeTeardown.some(hook =>
+          hook(this.current, this.nodes, this.scope!),
+        )
+      }
+      if (!preserveScope) {
         this.scope.stop()
       }
       const mode = transition && transition.mode
       if (mode) {
         applyTransitionLeaveHooks(this.nodes, transition, () =>
-          this.render(render, instance, transition, parent),
+          this.render(render, transition, parent),
         )
         parent && remove(this.nodes, parent)
         if (mode === 'out-in') {
@@ -123,7 +131,7 @@ export class DynamicFragment extends VaporFragment {
       }
     }
 
-    this.render(render, instance, transition, parent)
+    this.render(render, transition, parent)
 
     if (this.fallback) {
       // set fallback for nested fragments
@@ -155,32 +163,36 @@ export class DynamicFragment extends VaporFragment {
 
   private render(
     render: BlockFn | undefined,
-    instance: GenericComponentInstance,
     transition: VaporTransitionHooks | undefined,
     parent: ParentNode | null,
   ) {
     if (render) {
-      // For KeepAlive, try to reuse the keepAlive scope for this key
-      const scope =
-        this.inKeepAlive && this.keptAliveScopes
-          ? this.keptAliveScopes.get(this.current)
-          : undefined
+      // try to reuse the kept-alive scope
+      const scope = this.getScope && this.getScope(this.current)
       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 (this.inKeepAlive) {
-        ;(instance as KeepAliveInstance).cacheFragment(this)
-      }
+
       if (transition) {
         this.$transition = applyTransitionHooks(this.nodes, transition)
       }
-      if (parent) insert(this.nodes, parent, this.anchor)
+
+      if (this.beforeMount) {
+        this.beforeMount.forEach(hook =>
+          hook(this.current, this.nodes, this.scope!),
+        )
+      }
+
+      if (parent) {
+        insert(this.nodes, parent, this.anchor)
+        if (this.mounted) {
+          this.mounted.forEach(hook => hook(this.nodes, this.scope!))
+        }
+      }
     } else {
       this.scope = undefined
       this.nodes = []
@@ -287,3 +299,9 @@ function findInvalidFragment(fragment: VaporFragment): VaporFragment | null {
 export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
   return val instanceof VaporFragment
 }
+
+export function isDynamicFragment(
+  val: NonNullable<unknown>,
+): val is DynamicFragment {
+  return val instanceof DynamicFragment
+}