Procházet zdrojové kódy

fix(keep-alive): avoid allocating composite keys on cache lookup

daiwei před 1 měsícem
rodič
revize
981bd83a51

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

@@ -23,6 +23,7 @@ import {
   createDynamicComponent,
   createFor,
   createIf,
+  createKeyedFragment,
   createSlot,
   createTemplateRefSetter,
   createVaporApp,
@@ -2002,6 +2003,88 @@ describe('VaporKeepAlive', () => {
     expect(keyA2).not.toBe(keyA1)
   })
 
+  test('should not retain composite key entries for uncached keyed branches', async () => {
+    const Comp = defineVaporComponent({
+      name: 'Comp',
+      setup() {
+        return template('<div></div>')()
+      },
+    })
+
+    const include = ref('OtherComp')
+    const routeKey = ref('a')
+
+    const rawSet = Map.prototype.set
+    const rawDelete = Map.prototype.delete
+    const compositeMaps = new WeakSet<Map<any, any>>()
+    let compositeSetCount = 0
+    let compositeDeleteCount = 0
+
+    const setSpy = vi.spyOn(Map.prototype, 'set').mockImplementation(function (
+      this: Map<any, any>,
+      key: any,
+      value: any,
+    ) {
+      if (
+        value &&
+        typeof value === 'object' &&
+        'branchKey' in value &&
+        'type' in value &&
+        value.type === Comp
+      ) {
+        compositeMaps.add(this)
+        compositeSetCount++
+      }
+      return rawSet.call(this, key, value)
+    })
+
+    const deleteSpy = vi
+      .spyOn(Map.prototype, 'delete')
+      .mockImplementation(function (this: Map<any, any>, key: any) {
+        if (compositeMaps.has(this)) {
+          compositeDeleteCount++
+        }
+        return rawDelete.call(this, key)
+      })
+
+    try {
+      const { instance } = define({
+        setup() {
+          return createComponent(
+            VaporKeepAlive,
+            { include: () => include.value },
+            {
+              default: () =>
+                createKeyedFragment(
+                  () => routeKey.value,
+                  () => createComponent(Comp),
+                ),
+            },
+          )
+        },
+      }).render()
+
+      const keepAliveInstance = instance!.block as any
+      const cache = keepAliveInstance.__v_cache as Map<any, any>
+
+      await nextTick()
+      expect(cache.size).toBe(0)
+
+      routeKey.value = 'b'
+      await nextTick()
+      expect(cache.size).toBe(0)
+
+      routeKey.value = 'c'
+      await nextTick()
+      expect(cache.size).toBe(0)
+
+      expect(compositeSetCount - compositeDeleteCount).toBeLessThanOrEqual(1)
+    } finally {
+      setSpy.mockRestore()
+      deleteSpy.mockRestore()
+    }
+  })
+
   test('handle error in async onActivated', async () => {
     const err = new Error('foo')
     const handler = vi.fn()

+ 47 - 3
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -95,7 +95,7 @@ type CompositeKey = {
 // Caches are passed as parameters (per KeepAlive instance) to avoid
 // module-level Map leaks for primitive type keys (e.g. string tag names
 // from VDOM interop).
-function getCompositeKey(
+function getOrCreateCompositeKey(
   type: BaseCacheKey,
   branchKey: any,
   compositeKeyCache: WeakMap<object, Map<any, CompositeKey>>,
@@ -119,6 +119,20 @@ function getCompositeKey(
   return composite
 }
 
+function getCompositeKey(
+  type: BaseCacheKey,
+  branchKey: any,
+  compositeKeyCache: WeakMap<object, Map<any, CompositeKey>>,
+  compositeKeyCachePrimitive: Map<any, Map<any, CompositeKey>>,
+): CacheKey | undefined {
+  const isObjectType = isObject(type) || isFunction(type)
+  const perType = isObjectType
+    ? compositeKeyCache.get(type)
+    : compositeKeyCachePrimitive.get(type)
+
+  return perType && perType.get(branchKey)
+}
+
 function deleteCompositeKey(
   key: CacheKey,
   compositeKeyCache: WeakMap<object, Map<any, CompositeKey>>,
@@ -183,6 +197,30 @@ const VaporKeepAliveImpl = defineVaporComponent({
       key?: any,
       branchKey?: any,
     ): CacheKey => {
+      if (key != null) {
+        return getOrCreateCompositeKey(
+          type,
+          key,
+          compositeKeyCache,
+          compositeKeyCachePrimitive,
+        )
+      }
+      if (branchKey !== undefined) {
+        return getOrCreateCompositeKey(
+          type,
+          branchKey,
+          compositeKeyCache,
+          compositeKeyCachePrimitive,
+        )
+      }
+      return type as CacheKey
+    }
+
+    const resolveKeyForLookup = (
+      type: BaseCacheKey,
+      key?: any,
+      branchKey?: any,
+    ): CacheKey | undefined => {
       if (key != null) {
         return getCompositeKey(
           type,
@@ -252,9 +290,15 @@ const VaporKeepAliveImpl = defineVaporComponent({
       getStorageContainer: () => storageContainer,
       getCachedComponent: (comp, key) => {
         if (isInteropEnabled && isVNode(comp)) {
-          return cache.get(resolveKey(comp.type, comp.key, currentBranchKey))
+          const cacheKey = resolveKeyForLookup(
+            comp.type,
+            comp.key,
+            currentBranchKey,
+          )
+          return cacheKey === undefined ? undefined : cache.get(cacheKey)
         }
-        return cache.get(resolveKey(comp, key, currentBranchKey))
+        const cacheKey = resolveKeyForLookup(comp, key, currentBranchKey)
+        return cacheKey === undefined ? undefined : cache.get(cacheKey)
       },
       activate: (instance, parentNode, anchor) => {
         current = instance