Przeglądaj źródła

fix(keep-alive): fix keyed branch scope leak in KeepAlive

daiwei 3 miesięcy temu
rodzic
commit
cbe905e1f3

+ 62 - 0
packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts

@@ -1701,6 +1701,68 @@ describe('VaporKeepAlive', () => {
     expect(keptAliveScopes.size).toBe(0)
   })
 
+  test('should stop branch scope when cache entry is pruned (keyed branches)', async () => {
+    const Comp = defineVaporComponent({
+      name: 'Comp',
+      props: ['id'],
+      setup(props: any) {
+        return template('<div></div>')()
+      },
+    })
+
+    const exclude = ref('')
+    const toggle = ref(true)
+    const { html, instance } = define({
+      setup() {
+        return createComponent(
+          VaporKeepAlive,
+          { exclude: () => exclude.value },
+          {
+            default: () =>
+              // index=0 makes this a keyed DynamicFragment
+              createIf(
+                () => toggle.value,
+                () => createComponent(Comp, { id: () => 'a' }),
+                () => createComponent(Comp, { id: () => 'b' }),
+                undefined,
+                undefined,
+                0,
+              ),
+          },
+        )
+      },
+    }).render()
+
+    const keepAliveInstance = instance!.block as any
+    const cache = keepAliveInstance.__v_cache as Map<any, any>
+    const keptAliveScopes = keepAliveInstance.__v_keptAliveScopes as Map<
+      any,
+      any
+    >
+
+    expect(html()).toBe('<div></div><!--if-->')
+
+    // switch from branch A to branch B
+    toggle.value = false
+    await nextTick()
+
+    // both branches should be independently cached
+    // keyed branches use branchKey to distinguish same-type components
+    expect(cache.size).toBe(2)
+    expect(keptAliveScopes.size).toBe(2)
+
+    // switch back to branch A
+    toggle.value = true
+    await nextTick()
+    expect(cache.size).toBe(2)
+
+    // prune by excluding Comp — all entries should be cleaned
+    exclude.value = 'Comp'
+    await nextTick()
+    expect(cache.size).toBe(0)
+    expect(keptAliveScopes.size).toBe(0)
+  })
+
   test('handle error in async onActivated', async () => {
     const err = new Error('foo')
     const handler = vi.fn()

+ 6 - 1
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -270,6 +270,11 @@ const VaporKeepAliveImpl = defineVaporComponent({
         ) {
           return
         }
+        // For keyed DynamicFragment, read branch key from the fragment
+        // since currentBranchKey is already restored at lifecycle hook time
+        if (block.keyed) {
+          currentBranchKey = block.current
+        }
       }
       const [innerBlock, interop] = getInnerBlock(block)
       if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
@@ -319,7 +324,7 @@ const VaporKeepAliveImpl = defineVaporComponent({
 
     // delete scope from keptAliveScopes by one key,
     // also removes the paired entry (cache key ↔ branch key)
-    const deleteScope = (key: CacheKey): EffectScope | undefined => {
+    const deleteScope = (key: any): EffectScope | undefined => {
       const scope = keptAliveScopes.get(key)
       if (scope) {
         keptAliveScopes.delete(key)

+ 20 - 11
packages/runtime-vapor/src/fragment.ts

@@ -153,12 +153,19 @@ export class DynamicFragment extends VaporFragment {
       const keepAliveCtx = this.keepAliveCtx
 
       // if keepAliveCtx exists and processShapeFlag returns a cache key,
-      // cache the scope and retain it
+      // cache the scope and retain it.
+      // For keyed fragments, temporarily set branchKey to prevKey so
+      // getCacheKey generates the correct composite key for the OLD block.
+      const needBranchKey = keepAliveCtx && this.keyed
+      const prevBranchKey = needBranchKey
+        ? keepAliveCtx.setCurrentBranchKey(prevKey)
+        : undefined
       const cacheKey = keepAliveCtx && keepAliveCtx.processShapeFlag(this.nodes)
       if (cacheKey) {
         keepAliveCtx!.cacheScope(cacheKey, prevKey, this.scope)
         retainScope = true
       }
+      if (needBranchKey) keepAliveCtx.setCurrentBranchKey(prevBranchKey)
 
       if (!retainScope) {
         this.scope.stop()
@@ -230,19 +237,21 @@ export class DynamicFragment extends VaporFragment {
       try {
         this.nodes = this.runWithRenderCtx(() => this.scope!.run(render) || [])
       } finally {
-        if (needBranchKey) keepAliveCtx.setCurrentBranchKey(prevBranchKey)
-      }
+        // set key on blocks
+        if (this.keyed) setKey(this.nodes, this.current)
 
-      // set key on blocks
-      if (this.keyed) setKey(this.nodes, this.current)
+        if (transition) {
+          this.$transition = applyTransitionHooks(this.nodes, transition)
+        }
 
-      if (transition) {
-        this.$transition = applyTransitionHooks(this.nodes, transition)
-      }
+        // call processShapeFlag to mark shapeFlag before mounting.
+        // Must be called before restoring branchKey so getCacheKey
+        // generates the correct composite key.
+        if (keepAliveCtx) {
+          keepAliveCtx.processShapeFlag(this.nodes)
+        }
 
-      // call processShapeFlag to mark shapeFlag before mounting
-      if (keepAliveCtx) {
-        keepAliveCtx.processShapeFlag(this.nodes)
+        if (needBranchKey) keepAliveCtx.setCurrentBranchKey(prevBranchKey)
       }
 
       if (parent) {