From dfaa5bb9b3349b733f10e886ab20b1e776254615 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 30 Oct 2025 17:35:15 +0800 Subject: [PATCH 1/6] feat: add support for async components in KeepAlive --- .../__tests__/apiDefineAsyncComponent.spec.ts | 103 +++++++++++++++++- .../__tests__/components/KeepAlive.spec.ts | 78 ++++++++++++- .../src/apiDefineAsyncComponent.ts | 10 +- packages/runtime-vapor/src/component.ts | 9 +- .../runtime-vapor/src/components/KeepAlive.ts | 84 +++++++++++--- packages/runtime-vapor/src/vdomInterop.ts | 4 +- 6 files changed, 262 insertions(+), 26 deletions(-) diff --git a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts index fa7f481707c..7c0fb07ae5e 100644 --- a/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts +++ b/packages/runtime-vapor/__tests__/apiDefineAsyncComponent.spec.ts @@ -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('') + + await nextTick() + resolve!( + defineVaporComponent({ + setup() { + onActivated(() => { + spy() + }) + return template('Foo')() + }, + }), + ) + + await timeout() + expect(html()).toBe('Foo') + expect(spy).toBeCalledTimes(1) - test.todo('with KeepAlive + include', async () => {}) + toggle.value = false + await timeout() + expect(html()).toBe('Bar') + }) + + 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('') + + await nextTick() + resolve!( + defineVaporComponent({ + name: 'Foo', + setup() { + onActivated(() => { + spy() + }) + return template('Foo')() + }, + }), + ) + + await timeout() + expect(html()).toBe('Foo') + expect(spy).toBeCalledTimes(1) + }) }) diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts index 53d47651bbb..26c00bd3747 100644 --- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -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(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(``) + + resolve!( + defineVaporComponent({ + name: 'Foo', + setup(_, { expose }) { + const count = ref(0) + expose({ + inc: () => { + count.value++ + }, + }) + + const n0 = template(`

`)() as any + const x0 = child(n0) as any + renderEffect(() => { + setText(x0, String(count.value)) + }) + return n0 + }, + }), + ) + + await timeout() + // resolved + expect(html()).toBe(`

0

`) + + // change state + toggle out + instanceRef.value.inc() + toggle.value = false + await nextTick() + expect(html()).toBe('') + + // toggle in, state should be maintained + toggle.value = true + await nextTick() + expect(html()).toBe('

1

') + + toggle.value = false + await nextTick() + expect(html()).toBe('') + }) test('handle error in async onActivated', async () => { const err = new Error('foo') diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index dd6143950e3..15d03a2daf3 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -5,6 +5,7 @@ import { createAsyncComponentContext, currentInstance, handleError, + isKeepAlive, markAsyncBoundary, performAsyncHydrate, useAsyncComponentState, @@ -28,6 +29,7 @@ import { import { invokeArrayFns } from '@vue/shared' import { insert, remove } from './block' import { parentNode } from './dom/node' +import type { KeepAliveInstance } from './components/KeepAlive' /*@ __NO_SIDE_EFFECTS__ */ export function defineVaporAsyncComponent( @@ -120,7 +122,7 @@ export function defineVaporAsyncComponent( // already resolved let resolvedComp = getResolvedComp() if (resolvedComp) { - frag!.update(() => createInnerComp(resolvedComp!, instance)) + frag!.update(() => createInnerComp(resolvedComp!, instance, frag)) return frag } @@ -189,6 +191,12 @@ 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) + } + // set ref // @ts-expect-error frag && frag.setRef && frag.setRef(instance) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5b18c465104..81769a5dcb2 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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 } diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 3367ea089bd..574c25f94e0 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -1,8 +1,11 @@ import { + type GenericComponentInstance, type KeepAliveProps, currentInstance, devtoolsComponentAdded, getComponentName, + isAsyncWrapper, + isKeepAlive, matches, onBeforeUnmount, onMounted, @@ -32,6 +35,7 @@ export interface KeepAliveInstance extends VaporComponentInstance { ) => void deactivate: (instance: VaporComponentInstance) => void process: (block: Block) => void + cacheComponent: (instance: VaporComponentInstance) => void getCachedComponent: ( comp: VaporComponent, ) => VaporComponentInstance | VaporFragment | undefined @@ -66,22 +70,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,14 +105,25 @@ 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 + + const key = innerBlock.type + const blockToCache = + isFragment(block) && isFragment(block.nodes) + ? // cache the fragment nodes for vdom interop + block.nodes + : innerBlock + + innerCacheBlock(key, blockToCache) } onMounted(cacheBlock) @@ -129,12 +152,22 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ }) keepAliveInstance.getStorageContainer = () => storageContainer - keepAliveInstance.getCachedComponent = comp => cache.get(comp) + keepAliveInstance.getCachedComponent = comp => { + // For async components, use the resolved component type as the cache key + const key = (comp as any).__asyncResolved || comp + return cache.get(key) + } const processShapeFlag = (keepAliveInstance.process = block => { const instance = getInnerComponent(block) if (!instance) return + // For unresolved async wrappers, skip processing + // Wait for resolution and re-process via createInnerComp + if (isAsyncWrapper(instance) && !instance.type.__asyncResolved) { + return + } + if (cache.has(instance.type)) { instance.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE } @@ -144,6 +177,12 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ } }) + keepAliveInstance.cacheComponent = (instance: VaporComponentInstance) => { + if (!shouldCache(instance)) return + processShapeFlag(instance) + innerCacheBlock(instance.type, instance) + } + keepAliveInstance.activate = (instance, parentNode, anchor) => { current = instance activate(instance, parentNode, anchor) @@ -260,3 +299,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 +} diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index a44c078e6aa..ad7ab17dd52 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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`) @@ -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, From ca286fb469da59244d861035c3db30449ee5bc82 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 31 Oct 2025 10:22:09 +0800 Subject: [PATCH 2/6] wip: save --- .../src/apiDefineAsyncComponent.ts | 2 - packages/runtime-vapor/src/vdomInterop.ts | 57 +++++++++---------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 15d03a2daf3..687f4317911 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -149,8 +149,6 @@ export function defineVaporAsyncComponent( 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) diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index ad7ab17dd52..b0476b4cd7d 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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 From f96634928b6f02b58d97281da80fb6f770bb801d Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 31 Oct 2025 14:57:28 +0800 Subject: [PATCH 3/6] wip: save --- .../__tests__/components/KeepAlive.spec.ts | 34 ++++++++++++++++++- .../runtime-vapor/src/components/KeepAlive.ts | 13 ++++--- packages/runtime-vapor/src/vdomInterop.ts | 2 +- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts index 26c00bd3747..20a857be72c 100644 --- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -1269,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(`
hi
`) + assertHookCalls(oneHooks, [1, 1, 1, 0, 0]) + }) + + test('with v-if', async () => { const VdomComp = { setup() { const msg = ref('vdom') diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 574c25f94e0..9d3cabcafda 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -118,10 +118,12 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ const key = innerBlock.type const blockToCache = - isFragment(block) && isFragment(block.nodes) + isFragment(block) && isVdomInteropFragment(block.nodes) ? // cache the fragment nodes for vdom interop block.nodes - : innerBlock + : isVdomInteropFragment(block) + ? block + : innerBlock innerCacheBlock(key, blockToCache) } @@ -205,6 +207,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ // the `shapeFlag` is processed in `DynamicFragment.update`. Here only need // to process the `VaporComponentInstance` if (isVaporComponent(children)) processShapeFlag(children) + else if (isVdomInteropFragment(children)) { + children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + } function pruneCache(filter: (name: string) => boolean) { cache.forEach((instance, key) => { @@ -257,9 +262,9 @@ function getInnerBlock(block: Block): VaporComponentInstance | undefined { function getInnerComponent(block: Block): VaporComponentInstance | undefined { if (isVaporComponent(block)) { return block - } else if (isVdomInteropFragment(block)) { + } else if ((block as any as GenericComponentInstance).vnode) { // vdom interop - return block.vnode as any + return (block as any as GenericComponentInstance).vnode as any } } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index b0476b4cd7d..c516f64c17a 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -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) From 02b4629787db43abf06b443527f13e4c67657e14 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 31 Oct 2025 17:40:32 +0800 Subject: [PATCH 4/6] wip: save --- packages/runtime-vapor/src/block.ts | 8 + .../runtime-vapor/src/components/KeepAlive.ts | 193 +++++++++++++----- packages/runtime-vapor/src/fragment.ts | 8 +- 3 files changed, 157 insertions(+), 52 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 729ebee16fc..0ea8348230a 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -149,6 +149,14 @@ 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 pausedScopes = (block as DynamicFragment).pausedScopes + if (pausedScopes) { + for (let i = 0; i < pausedScopes.length; i++) { + pausedScopes[i].stop() + } + pausedScopes.length = 0 + } } } } diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index 9d3cabcafda..e51281f35fd 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -1,6 +1,7 @@ import { type GenericComponentInstance, type KeepAliveProps, + type VNode, currentInstance, devtoolsComponentAdded, getComponentName, @@ -25,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: ( @@ -34,15 +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 type Keys = Set @@ -116,54 +122,55 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ const innerBlock = getInnerBlock(block)! if (!innerBlock || !shouldCache(innerBlock)) return - const key = innerBlock.type - const blockToCache = - isFragment(block) && isVdomInteropFragment(block.nodes) - ? // cache the fragment nodes for vdom interop - block.nodes - : isVdomInteropFragment(block) - ? block - : innerBlock - - innerCacheBlock(key, blockToCache) + 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 = getCacheKey(frag) + } 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 = getCacheKey(current) + 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 => { // For async components, use the resolved component type as the cache key - const key = (comp as any).__asyncResolved || comp - return cache.get(key) + return cache.get(comp.__asyncResolved || comp) } - const processShapeFlag = (keepAliveInstance.process = block => { - const instance = getInnerComponent(block) - if (!instance) return - + const setShapeFlags = (instance: VaporComponentInstance) => { // For unresolved async wrappers, skip processing // Wait for resolution and re-process via createInnerComp if (isAsyncWrapper(instance) && !instance.type.__asyncResolved) { @@ -177,14 +184,51 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ if (shouldCache(instance)) { instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE } - }) + } keepAliveInstance.cacheComponent = (instance: VaporComponentInstance) => { if (!shouldCache(instance)) return - processShapeFlag(instance) + setShapeFlags(instance) innerCacheBlock(instance.type, instance) } + keepAliveInstance.processFragment = (frag: DynamicFragment) => { + const innerBlock = getInnerBlock(frag.nodes) + if (!innerBlock) return + + const fragment = findInteropFragment(frag.nodes) + if (fragment) { + setVdomShapeFlags(fragment) + } else { + setShapeFlags(innerBlock) + } + } + + keepAliveInstance.cacheFragment = (fragment: DynamicFragment) => { + // Find the component within the fragment + const innerBlock = getInnerBlock(fragment.nodes) + if (!innerBlock || !shouldCache(innerBlock)) 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) { + // For vdom components, set shapeFlag + setVdomShapeFlags(frag) + toCache = frag + key = getCacheKey(frag)! + } else { + setShapeFlags(innerBlock) + toCache = innerBlock + key = innerBlock.type + } + + innerCacheBlock(key, toCache) + } + keepAliveInstance.activate = (instance, parentNode, anchor) => { current = instance activate(instance, parentNode, anchor) @@ -194,6 +238,34 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ deactivate(instance, storageContainer) } + function setVdomShapeFlags( + fragment: VaporFragment, + shouldKeepAlive: boolean = true, + ) { + if (shouldKeepAlive) { + fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + } + const fragKey = getCacheKey(fragment) + if (fragKey && cache.has(fragKey)) { + fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE + } + // Also set shapeFlag on the component instance if it exists + const vnode = fragment.vnode as any + if (vnode && vnode.component) { + vnode.component.shapeFlag = fragment.vnode!.shapeFlag + } + } + + 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__) { @@ -202,18 +274,18 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ return children } - // `children` could be either a `VaporComponentInstance` or a `DynamicFragment` - // (when using `v-if` or ``). For `DynamicFragment` children, - // the `shapeFlag` is processed in `DynamicFragment.update`. Here only need - // to process the `VaporComponentInstance` - if (isVaporComponent(children)) processShapeFlag(children) - else if (isVdomInteropFragment(children)) { - children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + // Process shapeFlag for vapor and vdom components + // DynamicFragment (v-if, ) is processed in DynamicFragment.update + if (isVaporComponent(children)) { + setShapeFlags(children) + } else if (isInteropFragment(children)) { + setVdomShapeFlags(children, true) } 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) @@ -223,7 +295,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) @@ -251,7 +325,7 @@ function getInnerBlock(block: Block): VaporComponentInstance | undefined { if (isVaporComponent(block)) { return block } - if (isVdomInteropFragment(block)) { + if (isInteropFragment(block)) { return block.vnode as any } if (isFragment(block)) { @@ -259,17 +333,36 @@ function getInnerBlock(block: Block): VaporComponentInstance | undefined { } } -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 ((block as any as GenericComponentInstance).vnode) { - // vdom interop - return (block as any as GenericComponentInstance).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 +} + +function getCacheKey(block: VaporComponentInstance | VaporFragment): CacheKey { + if (isVaporComponent(block)) { + return block.type + } + + // vdom interop + return block.vnode!.type } export function activate( diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 07f1243e4e5..58da5d3097d 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -73,6 +73,7 @@ export class DynamicFragment extends VaporFragment { current?: BlockFn fallback?: BlockFn anchorLabel?: string + pausedScopes?: EffectScope[] constructor(anchorLabel?: string) { super([]) @@ -100,7 +101,10 @@ export class DynamicFragment extends VaporFragment { // teardown previous branch if (this.scope) { if (isKeepAlive(instance)) { - ;(instance as KeepAliveInstance).process(this.nodes) + ;(instance as KeepAliveInstance).processFragment(this) + // Pause the scope and store it for later cleanup + this.scope.pause() + ;(this.pausedScopes || (this.pausedScopes = [])).push(this.scope) } else { this.scope.stop() } @@ -159,7 +163,7 @@ export class DynamicFragment extends VaporFragment { this.scope = new EffectScope() this.nodes = this.scope.run(render) || [] if (isKeepAlive(instance)) { - ;(instance as KeepAliveInstance).process(this.nodes) + ;(instance as KeepAliveInstance).cacheFragment(this) } if (transition) { this.$transition = applyTransitionHooks(this.nodes, transition) From 050ca87c51e7f9a180fe88c9ec9500003c27abed Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 31 Oct 2025 22:39:10 +0800 Subject: [PATCH 5/6] wip: save --- .../__tests__/components/KeepAlive.spec.ts | 2 +- .../src/apiDefineAsyncComponent.ts | 2 + .../runtime-vapor/src/components/KeepAlive.ts | 86 ++++++------------- 3 files changed, 28 insertions(+), 62 deletions(-) diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts index 20a857be72c..09c4cbfc0c8 100644 --- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -1116,7 +1116,7 @@ describe('VaporKeepAlive', () => { // toggle in, state should be maintained toggle.value = true await nextTick() - expect(html()).toBe('

1

') + expect(html()).toBe('

1

') toggle.value = false await nextTick() diff --git a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts index 687f4317911..bd710271a11 100644 --- a/packages/runtime-vapor/src/apiDefineAsyncComponent.ts +++ b/packages/runtime-vapor/src/apiDefineAsyncComponent.ts @@ -193,6 +193,8 @@ function createInnerComp( // 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 ref diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index e51281f35fd..0582c93c688 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -128,7 +128,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ if (isFragment(block) && (frag = findInteropFragment(block))) { // vdom component: cache the fragment toCache = frag - key = getCacheKey(frag) + key = frag.vnode!.type } else { // vapor component: cache the instance toCache = innerBlock @@ -150,7 +150,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ // current instance will be unmounted as part of keep-alive's unmount if (current) { - const currentKey = getCacheKey(current) + const currentKey = isVaporComponent(current) + ? current.type + : current.vnode!.type if (currentKey === key) { // call deactivated hook const da = instance.da @@ -166,29 +168,12 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ keepAliveInstance.getStorageContainer = () => storageContainer keepAliveInstance.getCachedComponent = comp => { - // For async components, use the resolved component type as the cache key - return cache.get(comp.__asyncResolved || comp) - } - - const setShapeFlags = (instance: VaporComponentInstance) => { - // For unresolved async wrappers, skip processing - // Wait for resolution and re-process via createInnerComp - if (isAsyncWrapper(instance) && !instance.type.__asyncResolved) { - return - } - - if (cache.has(instance.type)) { - instance.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE - } - - if (shouldCache(instance)) { - instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - } + return cache.get(comp) } keepAliveInstance.cacheComponent = (instance: VaporComponentInstance) => { if (!shouldCache(instance)) return - setShapeFlags(instance) + instance.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE innerCacheBlock(instance.type, instance) } @@ -198,14 +183,23 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ const fragment = findInteropFragment(frag.nodes) if (fragment) { - setVdomShapeFlags(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 { - setShapeFlags(innerBlock) + 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) => { - // Find the component within the fragment const innerBlock = getInnerBlock(fragment.nodes) if (!innerBlock || !shouldCache(innerBlock)) return @@ -216,12 +210,11 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ // find vdom interop fragment const frag = findInteropFragment(fragment) if (frag) { - // For vdom components, set shapeFlag - setVdomShapeFlags(frag) + frag.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE toCache = frag - key = getCacheKey(frag)! + key = frag.vnode!.type } else { - setShapeFlags(innerBlock) + innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE toCache = innerBlock key = innerBlock.type } @@ -238,24 +231,6 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ deactivate(instance, storageContainer) } - function setVdomShapeFlags( - fragment: VaporFragment, - shouldKeepAlive: boolean = true, - ) { - if (shouldKeepAlive) { - fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - } - const fragKey = getCacheKey(fragment) - if (fragKey && cache.has(fragKey)) { - fragment.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE - } - // Also set shapeFlag on the component instance if it exists - const vnode = fragment.vnode as any - if (vnode && vnode.component) { - vnode.component.shapeFlag = fragment.vnode!.shapeFlag - } - } - function resetCachedShapeFlag( cached: VaporComponentInstance | VaporFragment, ) { @@ -277,9 +252,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ // Process shapeFlag for vapor and vdom components // DynamicFragment (v-if, ) is processed in DynamicFragment.update if (isVaporComponent(children)) { - setShapeFlags(children) + children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE } else if (isInteropFragment(children)) { - setVdomShapeFlags(children, true) + children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE } function pruneCache(filter: (name: string) => boolean) { @@ -324,11 +299,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ function getInnerBlock(block: Block): VaporComponentInstance | undefined { if (isVaporComponent(block)) { return block - } - if (isInteropFragment(block)) { + } else if (isInteropFragment(block)) { return block.vnode as any - } - if (isFragment(block)) { + } else if (isFragment(block)) { return getInnerBlock(block.nodes) } } @@ -356,15 +329,6 @@ function getInstanceFromCache( return cached.vnode!.component as GenericComponentInstance } -function getCacheKey(block: VaporComponentInstance | VaporFragment): CacheKey { - if (isVaporComponent(block)) { - return block.type - } - - // vdom interop - return block.vnode!.type -} - export function activate( instance: VaporComponentInstance, parentNode: ParentNode, From 6e44da67a0eca468e1c35bec603fd5f08c6c2f04 Mon Sep 17 00:00:00 2001 From: daiwei Date: Sat, 1 Nov 2025 21:10:47 +0800 Subject: [PATCH 6/6] wip: tweaks --- packages/runtime-vapor/src/block.ts | 11 ++++------ packages/runtime-vapor/src/fragment.ts | 28 ++++++++++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 0ea8348230a..cb86cd00990 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -149,13 +149,10 @@ 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 pausedScopes = (block as DynamicFragment).pausedScopes - if (pausedScopes) { - for (let i = 0; i < pausedScopes.length; i++) { - pausedScopes[i].stop() - } - pausedScopes.length = 0 + const scopes = (block as DynamicFragment).keptAliveScopes + if (scopes) { + scopes.forEach(scope => scope.stop()) + scopes.clear() } } } diff --git a/packages/runtime-vapor/src/fragment.ts b/packages/runtime-vapor/src/fragment.ts index 58da5d3097d..e30909ea067 100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@ -73,7 +73,8 @@ export class DynamicFragment extends VaporFragment { current?: BlockFn fallback?: BlockFn anchorLabel?: string - pausedScopes?: EffectScope[] + inKeepAlive?: boolean + keptAliveScopes?: Map constructor(anchorLabel?: string) { super([]) @@ -97,14 +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)) { + if (this.inKeepAlive) { ;(instance as KeepAliveInstance).processFragment(this) - // Pause the scope and store it for later cleanup - this.scope.pause() - ;(this.pausedScopes || (this.pausedScopes = [])).push(this.scope) + if (!this.keptAliveScopes) this.keptAliveScopes = new Map() + this.keptAliveScopes.set(this.current, this.scope) } else { this.scope.stop() } @@ -160,9 +160,21 @@ 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)) { + if (this.inKeepAlive) { ;(instance as KeepAliveInstance).cacheFragment(this) } if (transition) {