From: edison Date: Thu, 20 Nov 2025 07:51:02 +0000 (+0800) Subject: refactor(keep-alive): refactor KeepAlive to use new fragment hooks. (#14121) X-Git-Tag: v3.6.0-alpha.5~9 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=3a2cd69871c1b564526a2d0255e2e3b1a458394b;p=thirdparty%2Fvuejs%2Fcore.git refactor(keep-alive): refactor KeepAlive to use new fragment hooks. (#14121) --- diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 67ff4f0e91..6c968905a6 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -669,3 +669,8 @@ export { checkTransitionMode, leaveCbKey, } from './components/BaseTransition' + +/** + * @internal + */ +export type { GenericComponent } from './component' diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts index 7c0fb07ae5..a6ada37df0 100644 --- a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -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)), }, ) }, diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 25b99c987f..1b7c82bbef 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -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) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 27c9e33371..eb3a2bde2d 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -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() - } } } } diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 573fe613b9..2e285e6ab3 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -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() 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, ) 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 { diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index e30909ea06..d6a0f8b6c6 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -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 + + // 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): val is VaporFragment { return val instanceof VaporFragment } + +export function isDynamicFragment( + val: NonNullable, +): val is DynamicFragment { + return val instanceof DynamicFragment +}