assertHookCalls(twoHooks, [1, 1, 4, 4, 0]) // should remain inactive
})
+ test('should cache components in nested DynamicFragment (v-if > dynamic component)', async () => {
+ const outerIf = ref(true)
+ const viewRef = ref('one')
+
+ const { html } = define({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createIf(
+ () => outerIf.value,
+ () => createDynamicComponent(() => views[viewRef.value]),
+ ),
+ })
+ },
+ }).render()
+
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+ assertHookCalls(twoHooks, [0, 0, 0, 0, 0])
+
+ viewRef.value = 'two'
+ await nextTick()
+ expect(html()).toBe(`<div>two</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 1, 1, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 0, 0])
+
+ viewRef.value = 'one'
+ await nextTick()
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 2, 1, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+ // Toggle v-if off
+ outerIf.value = false
+ await nextTick()
+ expect(html()).toBe(`<!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 2, 2, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+ // Toggle v-if back on
+ outerIf.value = true
+ await nextTick()
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ // one should be reactivated from cache
+ assertHookCalls(oneHooks, [1, 1, 3, 2, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+ })
+
+ test('should cache components in nested DynamicFragment with initial false v-if', async () => {
+ const outerIf = ref(false)
+ const viewRef = ref('one')
+
+ const { html } = define({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createIf(
+ () => outerIf.value,
+ () => createDynamicComponent(() => views[viewRef.value]),
+ ),
+ })
+ },
+ }).render()
+
+ // Initially v-if is false, nothing rendered
+ expect(html()).toBe(`<!--if-->`)
+ assertHookCalls(oneHooks, [0, 0, 0, 0, 0])
+ assertHookCalls(twoHooks, [0, 0, 0, 0, 0])
+
+ // Toggle v-if on - component should mount and activate
+ outerIf.value = true
+ await nextTick()
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+ assertHookCalls(twoHooks, [0, 0, 0, 0, 0])
+
+ // Switch to component two
+ viewRef.value = 'two'
+ await nextTick()
+ expect(html()).toBe(`<div>two</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 1, 1, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 0, 0])
+
+ // Switch back to one - should be reactivated from cache
+ viewRef.value = 'one'
+ await nextTick()
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 2, 1, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+ // Toggle v-if off
+ outerIf.value = false
+ await nextTick()
+ expect(html()).toBe(`<!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 2, 2, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+ // Toggle v-if back on - should reactivate from cache
+ outerIf.value = true
+ await nextTick()
+ expect(html()).toBe(`<div>one</div><!--dynamic-component--><!--if-->`)
+ assertHookCalls(oneHooks, [1, 1, 3, 2, 0])
+ assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+ })
+
+ test('should cache async components in nested v-if', async () => {
+ const asyncOneHooks = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const AsyncOne = defineVaporAsyncComponent({
+ loader: () =>
+ Promise.resolve(
+ defineVaporComponent({
+ name: 'AsyncOne',
+ setup() {
+ onMounted(asyncOneHooks.mounted)
+ onActivated(asyncOneHooks.activated)
+ onDeactivated(asyncOneHooks.deactivated)
+ onUnmounted(asyncOneHooks.unmounted)
+ return template('<div>async one</div>')()
+ },
+ }),
+ ),
+ })
+
+ const outerIf = ref(true)
+
+ const { html } = define({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createIf(
+ () => outerIf.value,
+ () => createComponent(AsyncOne),
+ ),
+ })
+ },
+ }).render()
+
+ await timeout()
+ await nextTick()
+ expect(html()).toBe('<div>async one</div><!--async component--><!--if-->')
+ expect(asyncOneHooks.mounted).toHaveBeenCalledTimes(1)
+ expect(asyncOneHooks.activated).toHaveBeenCalledTimes(1)
+
+ // Toggle v-if off - should deactivate, not unmount
+ outerIf.value = false
+ await nextTick()
+ expect(html()).toBe('<!--if-->')
+ expect(asyncOneHooks.deactivated).toHaveBeenCalledTimes(1)
+ expect(asyncOneHooks.unmounted).toHaveBeenCalledTimes(0)
+
+ // Toggle back on - should reactivate from cache
+ outerIf.value = true
+ await nextTick()
+ expect(html()).toBe('<div>async one</div><!--async component--><!--if-->')
+ expect(asyncOneHooks.activated).toHaveBeenCalledTimes(2)
+ expect(asyncOneHooks.mounted).toHaveBeenCalledTimes(1) // not re-mounted
+ })
+
+ test('should cache components in deeply nested v-if (v-if > v-if > component)', async () => {
+ const compHooks = {
+ mounted: vi.fn(),
+ activated: vi.fn(),
+ deactivated: vi.fn(),
+ unmounted: vi.fn(),
+ }
+
+ const Comp = defineVaporComponent({
+ name: 'Comp',
+ setup() {
+ onMounted(compHooks.mounted)
+ onActivated(compHooks.activated)
+ onDeactivated(compHooks.deactivated)
+ onUnmounted(compHooks.unmounted)
+ return template('<div>comp</div>')()
+ },
+ })
+
+ const outerIf = ref(true)
+ const innerIf = ref(true)
+
+ const { html } = define({
+ setup() {
+ return createComponent(VaporKeepAlive, null, {
+ default: () =>
+ createIf(
+ () => outerIf.value,
+ () =>
+ createIf(
+ () => innerIf.value,
+ () => createComponent(Comp),
+ ),
+ ),
+ })
+ },
+ }).render()
+
+ expect(html()).toBe('<div>comp</div><!--if--><!--if-->')
+ expect(compHooks.mounted).toHaveBeenCalledTimes(1)
+ expect(compHooks.activated).toHaveBeenCalledTimes(1)
+
+ // Toggle inner v-if off
+ innerIf.value = false
+ await nextTick()
+ expect(html()).toBe('<!--if--><!--if-->')
+ expect(compHooks.deactivated).toHaveBeenCalledTimes(1)
+ expect(compHooks.unmounted).toHaveBeenCalledTimes(0)
+
+ // Toggle inner v-if back on - should reactivate
+ innerIf.value = true
+ await nextTick()
+ expect(html()).toBe('<div>comp</div><!--if--><!--if-->')
+ expect(compHooks.activated).toHaveBeenCalledTimes(2)
+ expect(compHooks.mounted).toHaveBeenCalledTimes(1)
+
+ // Toggle outer v-if off
+ outerIf.value = false
+ await nextTick()
+ expect(html()).toBe('<!--if-->')
+ expect(compHooks.deactivated).toHaveBeenCalledTimes(2)
+ expect(compHooks.unmounted).toHaveBeenCalledTimes(0)
+
+ // Toggle outer v-if back on - should reactivate
+ outerIf.value = true
+ await nextTick()
+ expect(html()).toBe('<div>comp</div><!--if--><!--if-->')
+ expect(compHooks.activated).toHaveBeenCalledTimes(3)
+ expect(compHooks.mounted).toHaveBeenCalledTimes(1)
+ })
+
async function assertNameMatch(props: LooseRawProps) {
const outerRef = ref(true)
const viewRef = ref('one')
;(keepAliveInstance as any).__v_cache = cache
}
+ // Clear cache and shapeFlags before HMR rerender so cached components
+ // can be properly unmounted
+ if (__DEV__) {
+ const rerender = keepAliveInstance.hmrRerender
+ keepAliveInstance.hmrRerender = () => {
+ cache.forEach(cached => resetCachedShapeFlag(cached))
+ cache.clear()
+ keys.clear()
+ keptAliveScopes.forEach(scope => scope.stop())
+ keptAliveScopes.clear()
+ storageContainer.innerHTML = ''
+ current = undefined
+ rerender!()
+ }
+ }
+
keepAliveInstance.ctx = {
getStorageContainer: () => storageContainer,
getCachedComponent: comp => cache.get(comp),
const cacheBlock = () => {
// TODO suspense
const block = keepAliveInstance.block!
+ // Skip caching during out-in transition leaving phase.
+ // The correct component will be cached after renderBranch completes
+ // via the Fragment's onUpdated hook.
+ if (isDynamicFragment(block)) {
+ const transition = block.$transition
+ if (
+ transition &&
+ transition.mode === 'out-in' &&
+ transition.state.isLeaving
+ ) {
+ return
+ }
+ }
const [innerBlock, interop] = getInnerBlock(block)
if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
innerCacheBlock(
const pruneCacheEntry = (key: CacheKey) => {
const cached = cache.get(key)!
- resetCachedShapeFlag(cached)
-
// don't unmount if the instance is the current one
- if (cached !== current) {
+ if (cached && (!current || cached !== current)) {
+ resetCachedShapeFlag(cached)
remove(cached)
+ } else if (current) {
+ resetCachedShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
},
)
- ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() =>
- processFragment(frag),
- )
+ ;(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 scope = keptAliveScopes.get(key)
if (scope) {
}
}
- // process shapeFlag
+ // 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)
+ }
+ processChildren(block.nodes)
+ }
+ }
+
if (isVaporComponent(children)) {
children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
- if (isAsyncWrapper(children)) {
- injectKeepAliveHooks(children.block as DynamicFragment)
- watchAsyncResolve(children)
- }
- } 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)
- watchAsyncResolve(children.nodes)
+ processChildren(children)
+ } else if (isFragment(children)) {
+ // vdom interop
+ if (children.vnode) {
+ children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
+ } else {
+ processChildren(children)
}
}