From: edison Date: Wed, 14 Jan 2026 06:03:47 +0000 (+0800) Subject: refactor(keep-alive): simplify caching via context pattern (#14311) X-Git-Tag: v3.6.0-beta.4~22 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=568cdbab08dc7afad75bf065453e51381cedb0cd;p=thirdparty%2Fvuejs%2Fcore.git refactor(keep-alive): simplify caching via context pattern (#14311) --- diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 4c63ca2141..0493e2ea1f 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -7,7 +7,6 @@ import { handleError, markAsyncBoundary, performAsyncHydrate, - shallowRef, useAsyncComponentState, watch, } from '@vue/runtime-dom' @@ -49,8 +48,6 @@ export function defineVaporAsyncComponent( }, } = createAsyncComponentContext(source) - const resolvedDef = shallowRef() - return defineVaporComponent({ name: 'VaporAsyncComponentWrapper', @@ -108,10 +105,8 @@ export function defineVaporAsyncComponent( ) }, - // 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( load() .then(() => { - resolvedDef.value = getResolvedComp()! loaded.value = true }) .catch(err => { @@ -174,7 +168,9 @@ export function defineVaporAsyncComponent( } 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 diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index efe6970b0a..6e78af9a64 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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) diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 4649a0b7a9..2e1e2a2797 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -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 } diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 7cb1fa7cba..bbe7b8ab0f 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -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)) } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 97cbcbed92..4b9e56bb4e 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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,