]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: save
authordaiwei <daiwei521@126.com>
Wed, 9 Apr 2025 02:53:14 +0000 (10:53 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 9 Apr 2025 02:53:14 +0000 (10:53 +0800)
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/KeepAlive.ts

index 42f8518bb9de720c9544f68ac5e17cce440235db..dc27e6e59d76bb44a68821f28cfde387027945ad 100644 (file)
@@ -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
index 890459ad6a4e96804cdaea461616bb6c37420084..d7829ec90f3232a9ae261232ff8ffb537bacc32a 100644 (file)
@@ -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<string, VaporComponent>
   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(`<div> </div>`)() 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(`<div> </div>`)() 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<any>(null)
@@ -119,4 +170,303 @@ describe('VaporKeepAlive', () => {
     await nextTick()
     expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
   })
+
+  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(
+      `<div>one</div><!--dynamic-component--><!--if-->`,
+    )
+    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(
+      `<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(root.innerHTML).toBe(
+      `<div>one</div><!--dynamic-component--><!--if-->`,
+    )
+    assertHookCalls(oneHooks, [1, 1, 2, 1, 0])
+    assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+    viewRef.value = 'two'
+    await nextTick()
+    expect(root.innerHTML).toBe(
+      `<div>two</div><!--dynamic-component--><!--if-->`,
+    )
+    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(`<!--if-->`)
+    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(
+      `<div>one</div><!--dynamic-component--><!--if-->`,
+    )
+    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(`<!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 1, 1, 1])
+    assertHookCalls(twoHooks, [0, 0, 0, 0, 0])
+
+    toggle.value = true
+    await nextTick()
+    expect(root.innerHTML).toBe(
+      `<div>one</div><!--dynamic-component--><!--if-->`,
+    )
+    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(
+      `<div>two</div><!--dynamic-component--><!--if-->`,
+    )
+    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(`<!--if-->`)
+    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(`<div>two</div><!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+    assertHookCalls(twoHooks, [1, 1, 1, 0, 0])
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe(`<!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 1, 1, 0])
+    assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>two</div><!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 2, 1, 0])
+    assertHookCalls(twoHooks, [1, 1, 2, 1, 0])
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe(`<!--if-->`)
+    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(`<div>two</div>`)()
+      },
+    })
+    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(`<div>two</div><!--if-->`)
+    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(`<div>two</div><!--if--><!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
+    assertHookCalls(twoHooks, [1, 1, 1, 0, 0])
+
+    toggle1.value = false
+    await nextTick()
+    expect(html()).toBe(`<!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 1, 1, 0])
+    assertHookCalls(twoHooks, [1, 1, 1, 1, 0])
+
+    toggle1.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>two</div><!--if--><!--if-->`)
+    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(`<!--if--><!--if-->`)
+    assertHookCalls(oneHooks, [1, 1, 2, 1, 0])
+    assertHookCalls(twoHooks, [1, 1, 2, 2, 0])
+
+    toggle2.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>two</div><!--if--><!--if-->`)
+    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(`<!--if-->`)
+    // 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(`<!--if-->`)
+    // 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(`<!--if-->`)
+    // 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(`<div>two</div><!--if--><!--if-->`)
+    // 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(`<!--if-->`)
+    // assertHookCalls(oneHooks, [1, 1, 3, 3, 0])
+    // // assertHookCalls(twoHooks, [1, 1, 4, 4, 0])
+
+    // toggle1.value = true
+    // await nextTick()
+    // expect(html()).toBe(`<!--if--><!--if-->`)
+    // assertHookCalls(oneHooks, [1, 1, 4, 3, 0])
+    // // assertHookCalls(twoHooks, [1, 1, 4, 4, 0]) // should remain inactive
+  })
 })
index 118ffe5406399753a4ed1c7a45d58a88e83331be..e4fbf8aa7d411a1d134e384b11ddda616f37834a 100644 (file)
@@ -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`)
index fe4463039870a51b751deb364820b42c280a2d5a..a861c5372b43c5d73a7d181931d7ed2c883aba13 100644 (file)
@@ -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)