From 61d6f4801becc59062d35a023c9b9926c07c212a Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 9 Apr 2025 10:53:14 +0800 Subject: [PATCH] wip: save --- .../runtime-core/src/components/KeepAlive.ts | 2 +- .../__tests__/components/KeepAlive.spec.ts | 400 ++++++++++++++++-- packages/runtime-vapor/src/component.ts | 9 + .../runtime-vapor/src/components/KeepAlive.ts | 35 +- 4 files changed, 411 insertions(+), 35 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 42f8518bb9..dc27e6e59d 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -456,7 +456,7 @@ function registerKeepAliveHook( let current = target.parent while (current && current.parent) { let parent = current.parent - if (isKeepAlive(parent.vapor ? (parent as any) : current.parent.vnode)) { + if (isKeepAlive(parent.vapor ? (parent as any) : parent.vnode)) { injectToKeepAliveRoot(wrappedHook, type, target, current) } current = current.parent diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts index 890459ad6a..d7829ec90f 100644 --- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -1,13 +1,3 @@ -import { type VaporComponent, createComponent } from '../../src/component' -import { makeRender } from '../_utils' -import { VaporKeepAlive } from '../../src/components/KeepAlive' -import { defineVaporComponent } from '../../src/apiDefineComponent' -import { child } from '../../src/dom/node' -import { setText } from '../../src/dom/prop' -import { template } from '../../src/dom/template' -import { renderEffect } from '../../src/renderEffect' -import { createTemplateRefSetter } from '../../src/apiTemplateRef' -import { createDynamicComponent } from '../../src/apiCreateDynamicComponent' import { nextTick, onActivated, @@ -17,6 +7,20 @@ import { onUnmounted, ref, } from 'vue' +import type { VaporComponent } from '../../src/component' +import { makeRender } from '../_utils' +import { + VaporKeepAlive, + child, + createComponent, + createDynamicComponent, + createIf, + createTemplateRefSetter, + defineVaporComponent, + renderEffect, + setText, + template, +} from '../../src' const define = makeRender() @@ -27,16 +31,35 @@ describe('VaporKeepAlive', () => { let views: Record let root: HTMLDivElement + type HookType = { + beforeMount: any + mounted: any + activated: any + deactivated: any + unmounted: any + } + + let oneHooks = {} as HookType + let oneTestHooks = {} as HookType + let twoHooks = {} as HookType + beforeEach(() => { root = document.createElement('div') + oneHooks = { + beforeMount: vi.fn(), + mounted: vi.fn(), + activated: vi.fn(), + deactivated: vi.fn(), + unmounted: vi.fn(), + } one = defineVaporComponent({ name: 'one', setup(_, { expose }) { - onBeforeMount(vi.fn()) - onMounted(vi.fn()) - onActivated(vi.fn()) - onDeactivated(vi.fn()) - onUnmounted(vi.fn()) + onBeforeMount(() => oneHooks.beforeMount()) + onMounted(() => oneHooks.mounted()) + onActivated(() => oneHooks.activated()) + onDeactivated(() => oneHooks.deactivated()) + onUnmounted(() => oneHooks.unmounted()) const msg = ref('one') expose({ setMsg: (m: string) => (msg.value = m) }) @@ -50,11 +73,11 @@ describe('VaporKeepAlive', () => { oneTest = defineVaporComponent({ name: 'oneTest', setup() { - onBeforeMount(vi.fn()) - onMounted(vi.fn()) - onActivated(vi.fn()) - onDeactivated(vi.fn()) - onUnmounted(vi.fn()) + onBeforeMount(() => oneTestHooks.beforeMount()) + onMounted(() => oneTestHooks.mounted()) + onActivated(() => oneTestHooks.activated()) + onDeactivated(() => oneTestHooks.deactivated()) + onUnmounted(() => oneTestHooks.unmounted()) const msg = ref('oneTest') const n0 = template(`
`)() as any @@ -63,14 +86,23 @@ describe('VaporKeepAlive', () => { return n0 }, }) + twoHooks = { + beforeMount: vi.fn(), + mounted: vi.fn(), + activated: vi.fn(), + deactivated: vi.fn(), + unmounted: vi.fn(), + } two = defineVaporComponent({ name: 'two', setup() { - onBeforeMount(vi.fn()) - onMounted(vi.fn()) - onActivated(vi.fn()) - onDeactivated(vi.fn()) - onUnmounted(vi.fn()) + onBeforeMount(() => twoHooks.beforeMount()) + onMounted(() => twoHooks.mounted()) + onActivated(() => { + twoHooks.activated() + }) + onDeactivated(() => twoHooks.deactivated()) + onUnmounted(() => twoHooks.unmounted()) const msg = ref('two') const n0 = template(`
`)() as any @@ -86,6 +118,25 @@ describe('VaporKeepAlive', () => { } }) + function assertHookCalls( + hooks: { + beforeMount: any + mounted: any + activated: any + deactivated: any + unmounted: any + }, + callCounts: number[], + ) { + expect([ + hooks.beforeMount.mock.calls.length, + hooks.mounted.mock.calls.length, + hooks.activated.mock.calls.length, + hooks.deactivated.mock.calls.length, + hooks.unmounted.mock.calls.length, + ]).toEqual(callCounts) + } + test('should preserve state', async () => { const viewRef = ref('one') const instanceRef = ref(null) @@ -119,4 +170,303 @@ describe('VaporKeepAlive', () => { await nextTick() expect(root.innerHTML).toBe(`
changed
`) }) + + test('should call correct lifecycle hooks', async () => { + const toggle = ref(true) + const viewRef = ref('one') + + const { mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => + createComponent(VaporKeepAlive as any, null, { + default: () => createDynamicComponent(() => views[viewRef.value]), + }), + ) + }, + }).create() + mount(root) + expect(root.innerHTML).toBe( + `
one
`, + ) + assertHookCalls(oneHooks, [1, 1, 1, 0, 0]) + assertHookCalls(twoHooks, [0, 0, 0, 0, 0]) + + // toggle kept-alive component + viewRef.value = 'two' + await nextTick() + expect(root.innerHTML).toBe( + `
two
`, + ) + assertHookCalls(oneHooks, [1, 1, 1, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 0, 0]) + + viewRef.value = 'one' + await nextTick() + expect(root.innerHTML).toBe( + `
one
`, + ) + assertHookCalls(oneHooks, [1, 1, 2, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 1, 0]) + + viewRef.value = 'two' + await nextTick() + expect(root.innerHTML).toBe( + `
two
`, + ) + assertHookCalls(oneHooks, [1, 1, 2, 2, 0]) + assertHookCalls(twoHooks, [1, 1, 2, 1, 0]) + + // teardown keep-alive, should unmount all components including cached + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe(``) + assertHookCalls(oneHooks, [1, 1, 2, 2, 1]) + assertHookCalls(twoHooks, [1, 1, 2, 2, 1]) + }) + + test('should call correct lifecycle hooks when toggle the KeepAlive first', async () => { + const toggle = ref(true) + const viewRef = ref('one') + + const { mount } = define({ + setup() { + return createIf( + () => toggle.value, + () => + createComponent(VaporKeepAlive as any, null, { + default: () => createDynamicComponent(() => views[viewRef.value]), + }), + ) + }, + }).create() + mount(root) + expect(root.innerHTML).toBe( + `
one
`, + ) + assertHookCalls(oneHooks, [1, 1, 1, 0, 0]) + assertHookCalls(twoHooks, [0, 0, 0, 0, 0]) + + // should unmount 'one' component when toggle the KeepAlive first + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe(``) + assertHookCalls(oneHooks, [1, 1, 1, 1, 1]) + assertHookCalls(twoHooks, [0, 0, 0, 0, 0]) + + toggle.value = true + await nextTick() + expect(root.innerHTML).toBe( + `
one
`, + ) + assertHookCalls(oneHooks, [2, 2, 2, 1, 1]) + assertHookCalls(twoHooks, [0, 0, 0, 0, 0]) + + // 1. the first time toggle kept-alive component + viewRef.value = 'two' + await nextTick() + expect(root.innerHTML).toBe( + `
two
`, + ) + assertHookCalls(oneHooks, [2, 2, 2, 2, 1]) + assertHookCalls(twoHooks, [1, 1, 1, 0, 0]) + + // 2. should unmount all components including cached + toggle.value = false + await nextTick() + expect(root.innerHTML).toBe(``) + assertHookCalls(oneHooks, [2, 2, 2, 2, 2]) + assertHookCalls(twoHooks, [1, 1, 1, 1, 1]) + }) + + test('should call lifecycle hooks on nested components', async () => { + const one = defineVaporComponent({ + name: 'one', + setup() { + onBeforeMount(() => oneHooks.beforeMount()) + onMounted(() => oneHooks.mounted()) + onActivated(() => oneHooks.activated()) + onDeactivated(() => oneHooks.deactivated()) + onUnmounted(() => oneHooks.unmounted()) + return createComponent(two) + }, + }) + const toggle = ref(true) + const { html } = define({ + setup() { + return createComponent(VaporKeepAlive as any, null, { + default() { + return createIf( + () => toggle.value, + () => + createComponent(one as any, null, { + default: () => createDynamicComponent(() => views['one']), + }), + ) + }, + }) + }, + }).render() + expect(html()).toBe(`
two
`) + assertHookCalls(oneHooks, [1, 1, 1, 0, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 0, 0]) + + toggle.value = false + await nextTick() + expect(html()).toBe(``) + assertHookCalls(oneHooks, [1, 1, 1, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 1, 0]) + + toggle.value = true + await nextTick() + expect(html()).toBe(`
two
`) + assertHookCalls(oneHooks, [1, 1, 2, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 2, 1, 0]) + + toggle.value = false + await nextTick() + expect(html()).toBe(``) + assertHookCalls(oneHooks, [1, 1, 2, 2, 0]) + assertHookCalls(twoHooks, [1, 1, 2, 2, 0]) + }) + + test('should call lifecycle hooks on nested components when root component no hooks', async () => { + const spy = vi.fn() + const two = defineVaporComponent({ + name: 'two', + setup() { + onActivated(() => spy()) + return template(`
two
`)() + }, + }) + const one = defineVaporComponent({ + name: 'one', + setup() { + return createComponent(two) + }, + }) + + const toggle = ref(true) + const { html } = define({ + setup() { + return createComponent(VaporKeepAlive as any, null, { + default() { + return createIf( + () => toggle.value, + () => createComponent(one), + ) + }, + }) + }, + }).render() + + expect(html()).toBe(`
two
`) + expect(spy).toHaveBeenCalledTimes(1) + }) + + test.todo('should call correct hooks for nested keep-alive', async () => { + const toggle2 = ref(true) + const one = defineVaporComponent({ + name: 'one', + setup() { + onBeforeMount(() => oneHooks.beforeMount()) + onMounted(() => oneHooks.mounted()) + onActivated(() => oneHooks.activated()) + onDeactivated(() => oneHooks.deactivated()) + onUnmounted(() => oneHooks.unmounted()) + return createComponent(VaporKeepAlive as any, null, { + default() { + return createIf( + () => toggle2.value, + () => createComponent(two), + ) + }, + }) + }, + }) + + const toggle1 = ref(true) + const { html } = define({ + setup() { + return createComponent(VaporKeepAlive as any, null, { + default() { + return createIf( + () => toggle1.value, + () => createComponent(one), + ) + }, + }) + }, + }).render() + + expect(html()).toBe(`
two
`) + assertHookCalls(oneHooks, [1, 1, 1, 0, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 0, 0]) + + toggle1.value = false + await nextTick() + expect(html()).toBe(``) + assertHookCalls(oneHooks, [1, 1, 1, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 1, 1, 0]) + + toggle1.value = true + await nextTick() + expect(html()).toBe(`
two
`) + assertHookCalls(oneHooks, [1, 1, 2, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 2, 1, 0]) + + // toggle nested instance + toggle2.value = false + await nextTick() + expect(html()).toBe(``) + assertHookCalls(oneHooks, [1, 1, 2, 1, 0]) + assertHookCalls(twoHooks, [1, 1, 2, 2, 0]) + + toggle2.value = true + await nextTick() + expect(html()).toBe(`
two
`) + assertHookCalls(oneHooks, [1, 1, 2, 1, 0]) + // problem is component one isDeactivated. leading to + // the activated hook of two is not called + assertHookCalls(twoHooks, [1, 1, 3, 2, 0]) + + // toggle1.value = false + // await nextTick() + // expect(html()).toBe(``) + // assertHookCalls(oneHooks, [1, 1, 2, 2, 0]) + // assertHookCalls(twoHooks, [1, 1, 3, 3, 0]) + + // // toggle nested instance when parent is deactivated + // toggle2.value = false + // await nextTick() + // expect(html()).toBe(``) + // assertHookCalls(oneHooks, [1, 1, 2, 2, 0]) + // // assertHookCalls(twoHooks, [1, 1, 3, 3, 0]) // should not be affected + + // toggle2.value = true + // await nextTick() + // expect(html()).toBe(``) + // assertHookCalls(oneHooks, [1, 1, 2, 2, 0]) + // // assertHookCalls(twoHooks, [1, 1, 3, 3, 0]) // should not be affected + + // toggle1.value = true + // await nextTick() + // expect(html()).toBe(`
two
`) + // assertHookCalls(oneHooks, [1, 1, 3, 2, 0]) + // // assertHookCalls(twoHooks, [1, 1, 4, 3, 0]) + + // toggle1.value = false + // toggle2.value = false + // await nextTick() + // expect(html()).toBe(``) + // assertHookCalls(oneHooks, [1, 1, 3, 3, 0]) + // // assertHookCalls(twoHooks, [1, 1, 4, 4, 0]) + + // toggle1.value = true + // await nextTick() + // expect(html()).toBe(``) + // assertHookCalls(oneHooks, [1, 1, 4, 3, 0]) + // // assertHookCalls(twoHooks, [1, 1, 4, 4, 0]) // should remain inactive + }) }) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 118ffe5406..e4fbf8aa7d 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -505,6 +505,7 @@ export function mountComponent( (parent as KeepAliveInstance).isKeptAlive(instance) ) { ;(parent as KeepAliveInstance).activate(instance, parentNode, anchor as any) + instance.isMounted = true return } @@ -514,6 +515,14 @@ export function mountComponent( if (instance.bm) invokeArrayFns(instance.bm) insert(instance.block, parentNode, anchor) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) + if ( + parent && + isKeepAlive(parent as any) && + (parent as KeepAliveInstance).shouldKeepAlive(instance) && + instance.a + ) { + queuePostFlushCb(instance.a!) + } instance.isMounted = true if (__DEV__) { endMeasure(instance, `mount`) diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index fe44630398..a861c5372b 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -4,6 +4,7 @@ import { devtoolsComponentAdded, getComponentName, invalidateMount, + isKeepAlive, matches, onBeforeUnmount, onMounted, @@ -12,11 +13,12 @@ import { warn, watch, } from '@vue/runtime-dom' -import { type Block, insert, isFragment, isValidBlock, remove } from '../block' +import { type Block, insert, isFragment, isValidBlock } from '../block' import { type VaporComponent, type VaporComponentInstance, isVaporComponent, + unmountComponent, } from '../component' import { defineVaporComponent } from '../apiDefineComponent' import { invokeArrayFns, isArray } from '@vue/shared' @@ -54,6 +56,8 @@ const VaporKeepAliveImpl = defineVaporComponent({ const cache: Cache = new Map() const keys: Keys = new Set() const storageContainer = document.createElement('div') + let current: VaporComponentInstance | undefined + let isUnmounting = false if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { ;(keepAliveInstance as any).__v_cache = cache @@ -63,10 +67,10 @@ const VaporKeepAliveImpl = defineVaporComponent({ function cacheBlock() { // TODO suspense - const current = keepAliveInstance.block! - if (!isValidBlock(current)) return + const currentBlock = keepAliveInstance.block! + if (!isValidBlock(currentBlock)) return - const block = getInnerBlock(current)! + const block = getInnerBlock(currentBlock)! if (!block) return const key = block.type @@ -81,12 +85,24 @@ const VaporKeepAliveImpl = defineVaporComponent({ pruneCacheEntry(keys.values().next().value!) } } - cache.set(key, block) + cache.set(key, (current = block)) } onMounted(cacheBlock) onUpdated(cacheBlock) - onBeforeUnmount(() => cache.forEach(cached => remove(cached))) + onBeforeUnmount(() => { + isUnmounting = true + cache.forEach(cached => { + cache.delete(cached.type) + // current instance will be unmounted as part of keep-alive's unmount + if (current && current.type === cached.type) { + const da = cached.da + da && queuePostFlushCb(da) + return + } + unmountComponent(cached, storageContainer) + }) + }) const children = slots.default() if (isArray(children) && children.length > 1) { @@ -101,8 +117,8 @@ const VaporKeepAliveImpl = defineVaporComponent({ parentNode: ParentNode, anchor: Node, ) => { - invalidateMount(instance.m) - invalidateMount(instance.a) + // invalidateMount(instance.m) + // invalidateMount(instance.a) const cachedBlock = cache.get(instance.type)! insert((instance.block = cachedBlock.block), parentNode, anchor) @@ -129,6 +145,7 @@ const VaporKeepAliveImpl = defineVaporComponent({ } keepAliveInstance.shouldKeepAlive = (instance: VaporComponentInstance) => { + if (isUnmounting) return false const name = getComponentName(instance.type) if ( (include && (!name || !matches(include, name))) || @@ -155,7 +172,7 @@ const VaporKeepAliveImpl = defineVaporComponent({ function pruneCacheEntry(key: CacheKey) { const cached = cache.get(key) if (cached) { - remove(cached) + unmountComponent(cached) } cache.delete(key) keys.delete(key) -- 2.47.2