]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(keep-alive): simplify caching via context pattern (#14311)
authoredison <daiwei521@126.com>
Wed, 14 Jan 2026 06:03:47 +0000 (14:03 +0800)
committerGitHub <noreply@github.com>
Wed, 14 Jan 2026 06:03:47 +0000 (14:03 +0800)
packages/runtime-vapor/src/apiDefineAsyncComponent.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 4c63ca2141a851d4394632f8b9716f3dcbe7ad82..0493e2ea1f7b0d62f3d0828e94537cb6270ff276 100644 (file)
@@ -7,7 +7,6 @@ import {
   handleError,
   markAsyncBoundary,
   performAsyncHydrate,
-  shallowRef,
   useAsyncComponentState,
   watch,
 } from '@vue/runtime-dom'
@@ -49,8 +48,6 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
     },
   } = createAsyncComponentContext<T, VaporComponent>(source)
 
-  const resolvedDef = shallowRef<VaporComponent>()
-
   return defineVaporComponent({
     name: 'VaporAsyncComponentWrapper',
 
@@ -108,10 +105,8 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
       )
     },
 
-    // this accessor tracks the internal shallowRef `resolvedDef`.
-    // this allows KeepAlive to watch the resolution status.
     get __asyncResolved() {
-      return resolvedDef.value
+      return getResolvedComp()
     },
 
     setup() {
@@ -153,7 +148,6 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
 
       load()
         .then(() => {
-          resolvedDef.value = getResolvedComp()!
           loaded.value = true
         })
         .catch(err => {
@@ -174,7 +168,9 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
         }
 
         if (instance.$transition) frag!.$transition = instance.$transition
-        frag!.update(render)
+        frag.update(render)
+        // Manually trigger cacheBlock for KeepAlive
+        if (frag.keepAliveCtx) frag.keepAliveCtx.cacheBlock()
       })
 
       return frag
index efe6970b0afae6d2a5a679a69771135f8a3413b2..6e78af9a645feb5717d0e3a9e2ab3114da310cf4 100644 (file)
@@ -107,6 +107,10 @@ import {
   isVaporTeleport,
 } from './components/Teleport'
 import type { KeepAliveInstance } from './components/KeepAlive'
+import {
+  currentKeepAliveCtx,
+  setCurrentKeepAliveCtx,
+} from './components/KeepAlive'
 import {
   insertionAnchor,
   insertionParent,
@@ -329,6 +333,16 @@ export function createComponent(
     once,
   )
 
+  // handle currentKeepAliveCtx for component boundary isolation
+  // AsyncWrapper should NOT clear currentKeepAliveCtx so its internal
+  // DynamicFragment can capture it
+  if (currentKeepAliveCtx && !isAsyncWrapper(instance)) {
+    currentKeepAliveCtx.processShapeFlag(instance)
+    // clear currentKeepAliveCtx so child components don't associate
+    // with parent's KeepAlive
+    setCurrentKeepAliveCtx(null)
+  }
+
   // reset currentSlotOwner to null to avoid affecting the child components
   const prevSlotOwner = setCurrentSlotOwner(null)
 
index 4649a0b7a9c6636d76a0bd3910513231443765bf..2e1e2a2797f90a190479b78a92ccc48906ab3800 100644 (file)
@@ -28,14 +28,28 @@ import {
 import { defineVaporComponent } from '../apiDefineComponent'
 import { ShapeFlags, invokeArrayFns, isArray } from '@vue/shared'
 import { createElement } from '../dom/node'
-import {
-  type DynamicFragment,
-  type VaporFragment,
-  isDynamicFragment,
-  isFragment,
-} from '../fragment'
+import { type VaporFragment, isDynamicFragment, isFragment } from '../fragment'
 import type { EffectScope } from '@vue/reactivity'
 
+export interface KeepAliveContext {
+  processShapeFlag(block: Block): boolean
+  cacheBlock(): void
+  cacheScope(key: any, scope: EffectScope): void
+  getScope(key: any): EffectScope | undefined
+}
+
+export let currentKeepAliveCtx: KeepAliveContext | null = null
+
+export function setCurrentKeepAliveCtx(
+  ctx: KeepAliveContext | null,
+): KeepAliveContext | null {
+  try {
+    return currentKeepAliveCtx
+  } finally {
+    currentKeepAliveCtx = ctx
+  }
+}
+
 export interface KeepAliveInstance extends VaporComponentInstance {
   ctx: {
     activate: (
@@ -154,8 +168,8 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       )
     }
 
-    const processFragment = (frag: DynamicFragment) => {
-      const [innerBlock, interop] = getInnerBlock(frag.nodes)
+    const processShapeFlag = (block: Block): boolean => {
+      const [innerBlock, interop] = getInnerBlock(block)
       if (!innerBlock || !shouldCache(innerBlock!, props, interop)) return false
 
       if (interop) {
@@ -237,95 +251,32 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       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.onBeforeTeardown || (frag.onBeforeTeardown = [])).push(
-        (oldKey, { retainScope }) => {
-          // if the fragment's nodes include a component that should be cached
-          // call retainScope() to avoid stopping the fragment's scope
-          if (processFragment(frag)) {
-            keptAliveScopes.set(oldKey, frag.scope!)
-            retainScope()
-          }
-        },
-      )
-      ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() => {
-        processFragment(frag)
-        // recursively inject hooks to nested DynamicFragments.
-        // this is necessary for cases like v-if > dynamic component where
-        // v-if starts as false - the nested DynamicFragment doesn't exist
-        // during initial setup, so we must inject hooks when v-if becomes true.
-        processChildren(frag.nodes)
-      })
-      // This ensures caching happens after renderBranch completes,
-      // since Vue's onUpdated fires before the deferred rendering finishes.
-      ;(frag.onUpdated || (frag.onUpdated = [])).push(() => {
-        if (frag.$transition && frag.$transition.mode === 'out-in') {
-          cacheBlock()
-        }
-      })
-      frag.getScope = key => {
+    const keepAliveCtx: KeepAliveContext = {
+      processShapeFlag,
+      cacheBlock,
+      cacheScope(key, scope) {
+        keptAliveScopes.set(key, scope)
+      },
+      getScope(key) {
         const scope = keptAliveScopes.get(key)
         if (scope) {
           keptAliveScopes.delete(key)
           return scope
         }
-      }
+      },
     }
 
-    // for unresolved async wrapper, we need to watch the __asyncResolved
-    // property and cache the resolved component once it resolves.
-    const watchAsyncResolve = (instance: VaporComponentInstance) => {
-      if (!instance.type.__asyncResolved) {
-        watch(
-          () => instance.type.__asyncResolved,
-          resolved => {
-            if (resolved) cacheBlock()
-          },
-          { once: true },
-        )
-      }
-    }
+    const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
+    let children = slots.default()
+    setCurrentKeepAliveCtx(prevCtx)
 
-    // recursively inject hooks to nested DynamicFragments and handle AsyncWrapper
-    const processChildren = (block: Block): void => {
-      // handle async wrapper
-      if (isVaporComponent(block) && isAsyncWrapper(block)) {
-        watchAsyncResolve(block)
-        // block.block is a DynamicFragment
-        processChildren(block.block)
-      } else if (isDynamicFragment(block)) {
-        // avoid injecting hooks multiple times
-        if (!block.getScope) {
-          // DynamicFragment triggers processFragment via onBeforeMount hook,
-          // which correctly handles shapeFlag marking for inner components.
-          injectKeepAliveHooks(block)
-          if (block.nodes) processFragment(block)
+    if (isArray(children)) {
+      children = children.filter(child => !(child instanceof Comment))
+      if (children.length > 1) {
+        if (__DEV__) {
+          warn(`KeepAlive should contain exactly one component child.`)
         }
-        processChildren(block.nodes)
-      }
-    }
-
-    if (isVaporComponent(children)) {
-      children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
-      processChildren(children)
-    } else if (isFragment(children)) {
-      // vdom interop
-      if (children.vnode) {
-        children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
-      } else {
-        processChildren(children)
+        return children
       }
     }
 
@@ -349,7 +300,7 @@ const shouldCache = (
   ) as GenericComponent & AsyncComponentInternalOptions
 
   // for unresolved async components, don't cache yet
-  // caching will be handled by the watcher in watchAsyncResolve
+  // caching will be handled by AsyncWrapper calling keepAliveCtx.cacheBlock()
   if (isAsync && !type.__asyncResolved) {
     return false
   }
index 7cb1fa7cba4c61414340b308c511b02069cea43f..bbe7b8ab0ff52ec2914fea9597feabe7b4cc7d53 100644 (file)
@@ -33,6 +33,11 @@ import {
 import { isArray } from '@vue/shared'
 import { renderEffect } from './renderEffect'
 import { currentSlotOwner, setCurrentSlotOwner } from './componentSlots'
+import {
+  type KeepAliveContext,
+  currentKeepAliveCtx,
+  setCurrentKeepAliveCtx,
+} from './components/KeepAlive'
 
 export class VaporFragment<
   T extends Block = Block,
@@ -87,15 +92,7 @@ export class DynamicFragment extends VaporFragment {
   // set ref for async wrapper
   setAsyncRef?: (instance: VaporComponentInstance) => void
 
-  // get the kept-alive scope when used in keep-alive
-  getScope?: (key: any) => EffectScope | undefined
-
-  // hooks
-  onBeforeTeardown?: ((
-    oldKey: any,
-    context: { retainScope: () => void },
-  ) => void)[]
-  onBeforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
+  keepAliveCtx: KeepAliveContext | null
 
   slotOwner: VaporComponentInstance | null
 
@@ -103,6 +100,7 @@ export class DynamicFragment extends VaporFragment {
     super([])
     this.keyed = keyed
     this.slotOwner = currentSlotOwner
+    this.keepAliveCtx = currentKeepAliveCtx
     if (isHydrating) {
       this.anchorLabel = anchorLabel
       locateHydrationNode()
@@ -137,14 +135,15 @@ export class DynamicFragment extends VaporFragment {
     // teardown previous branch
     if (this.scope) {
       let retainScope = false
-      const context = {
-        retainScope: () => (retainScope = true),
-      }
-      if (this.onBeforeTeardown) {
-        for (const teardown of this.onBeforeTeardown) {
-          teardown(prevKey, context)
-        }
+      const keepAliveCtx = this.keepAliveCtx
+
+      // if keepAliveCtx exists and processShapeFlag returns true,
+      // cache the scope and retain it
+      if (keepAliveCtx && keepAliveCtx.processShapeFlag(this.nodes)) {
+        keepAliveCtx.cacheScope(prevKey, this.scope)
+        retainScope = true
       }
+
       if (!retainScope) {
         this.scope.stop()
       }
@@ -218,8 +217,9 @@ export class DynamicFragment extends VaporFragment {
     instance: GenericComponentInstance | null,
   ): void {
     if (render) {
+      const keepAliveCtx = this.keepAliveCtx
       // try to reuse the kept-alive scope
-      const scope = this.getScope && this.getScope(this.current)
+      const scope = keepAliveCtx && keepAliveCtx.getScope(this.current)
       if (scope) {
         this.scope = scope
       } else {
@@ -228,11 +228,14 @@ export class DynamicFragment extends VaporFragment {
 
       // restore slot owner
       const prevOwner = setCurrentSlotOwner(this.slotOwner)
+      // set currentKeepAliveCtx so nested DynamicFragments and components can capture it
+      const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
       // switch current instance to parent instance during update
       // ensure that the parent instance is correct for nested components
       const prev = parent && instance ? setCurrentInstance(instance) : undefined
       this.nodes = this.scope.run(render) || []
       if (prev !== undefined) setCurrentInstance(...prev)
+      setCurrentKeepAliveCtx(prevCtx)
       setCurrentSlotOwner(prevOwner)
 
       // set key on nodes
@@ -242,10 +245,9 @@ export class DynamicFragment extends VaporFragment {
         this.$transition = applyTransitionHooks(this.nodes, transition)
       }
 
-      if (this.onBeforeMount) {
-        this.onBeforeMount.forEach(hook =>
-          hook(this.current, this.nodes, this.scope!),
-        )
+      // call processShapeFlag to mark shapeFlag before mounting
+      if (keepAliveCtx) {
+        keepAliveCtx.processShapeFlag(this.nodes)
       }
 
       if (parent) {
@@ -270,6 +272,13 @@ export class DynamicFragment extends VaporFragment {
         }
 
         insert(this.nodes, parent, this.anchor)
+
+        // For out-in transition, call cacheBlock after renderBranch completes
+        // because KeepAlive's onUpdated fires before the deferred rendering finishes
+        if (keepAliveCtx && transition && transition.mode === 'out-in') {
+          keepAliveCtx.cacheBlock()
+        }
+
         if (this.onUpdated) {
           this.onUpdated.forEach(hook => hook(this.nodes))
         }
index 97cbcbed92c7fe2803987bbd3eb00ac38f16c9a0..4b9e56bb4e4b60f077c03d00de4b9ebf00a1b04e 100644 (file)
@@ -77,7 +77,9 @@ import { setTransitionHooks as setVaporTransitionHooks } from './components/Tran
 import {
   type KeepAliveInstance,
   activate,
+  currentKeepAliveCtx,
   deactivate,
+  setCurrentKeepAliveCtx,
 } from './components/KeepAlive'
 import { setParentSuspense } from './components/Suspense'
 
@@ -403,6 +405,12 @@ function createVDOMComponent(
     component,
     rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
   ))
+
+  if (currentKeepAliveCtx) {
+    currentKeepAliveCtx.processShapeFlag(frag)
+    setCurrentKeepAliveCtx(null)
+  }
+
   const wrapper = new VaporComponentInstance(
     { props: component.props },
     rawProps as RawProps,