Explorar o código

fix(runtime-vapor): stabilize KeepAlive cache keys with branch-scoped composite keys

daiwei hai 2 meses
pai
achega
d207e9ee17

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

@@ -20,6 +20,7 @@ import {
   createComponent,
   createDynamicComponent,
   createIf,
+  createSlot,
   createTemplateRefSetter,
   createVaporApp,
   defineVaporAsyncComponent,
@@ -28,6 +29,7 @@ import {
   setText,
   template,
   vaporInteropPlugin,
+  withVaporCtx,
 } from '../../src'
 
 const define = makeRender()
@@ -187,6 +189,115 @@ describe('VaporKeepAlive', () => {
     expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
   })
 
+  test('should cache same component across branches', async () => {
+    const toggle = ref(true)
+    const instanceA = ref<any>(null)
+    const instanceB = ref<any>(null)
+
+    const { html } = define({
+      setup() {
+        const setRefA = createTemplateRefSetter()
+        const setRefB = createTemplateRefSetter()
+        return createComponent(VaporKeepAlive, null, {
+          default: () =>
+            createIf(
+              () => toggle.value,
+              () => {
+                const n0 = createComponent(one)
+                setRefA(n0, instanceA)
+                return n0
+              },
+              () => {
+                const n1 = createComponent(one)
+                setRefB(n1, instanceB)
+                return n1
+              },
+              undefined,
+              0,
+            ),
+        })
+      },
+    }).render()
+
+    expect(html()).toBe(`<div>one</div><!--if-->`)
+
+    instanceA.value.setMsg('A')
+    await nextTick()
+    expect(html()).toBe(`<div>A</div><!--if-->`)
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe(`<div>one</div><!--if-->`)
+
+    instanceB.value.setMsg('B')
+    await nextTick()
+    expect(html()).toBe(`<div>B</div><!--if-->`)
+
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>A</div><!--if-->`)
+  })
+
+  test('should cache same component across branches with reusable keep-alive', async () => {
+    const toggle = ref(true)
+    const instanceA = ref<any>(null)
+    const instanceB = ref<any>(null)
+
+    const Comp = defineVaporComponent({
+      setup() {
+        return createComponent(VaporKeepAlive, null, {
+          default: withVaporCtx(() => {
+            const n0 = createSlot('default', null)
+            return n0
+          }),
+        })
+      },
+    })
+
+    const { html } = define({
+      setup() {
+        const setRefA = createTemplateRefSetter()
+        const setRefB = createTemplateRefSetter()
+        return createComponent(Comp, null, {
+          default: () =>
+            createIf(
+              () => toggle.value,
+              () => {
+                const n0 = createComponent(one)
+                setRefA(n0, instanceA)
+                return n0
+              },
+              () => {
+                const n1 = createComponent(one)
+                setRefB(n1, instanceB)
+                return n1
+              },
+              undefined,
+              0,
+            ),
+        })
+      },
+    }).render()
+
+    expect(html()).toBe(`<div>one</div><!--if--><!--slot-->`)
+
+    instanceA.value.setMsg('A')
+    await nextTick()
+    expect(html()).toBe(`<div>A</div><!--if--><!--slot-->`)
+
+    toggle.value = false
+    await nextTick()
+    expect(html()).toBe(`<div>one</div><!--if--><!--slot-->`)
+
+    instanceB.value.setMsg('B')
+    await nextTick()
+    expect(html()).toBe(`<div>B</div><!--if--><!--slot-->`)
+
+    toggle.value = true
+    await nextTick()
+    expect(html()).toBe(`<div>A</div><!--if--><!--slot-->`)
+  })
+
   test('should call correct lifecycle hooks', async () => {
     const toggle = ref(true)
     const viewRef = ref('one')

+ 1 - 1
packages/runtime-vapor/src/apiCreateDynamicComponent.ts

@@ -49,7 +49,7 @@ export function createDynamicComponent(
         if (isKeepAlive(currentInstance)) {
           const frag = (
             currentInstance as KeepAliveInstance
-          ).ctx.getCachedComponent(value.type as any) as VaporFragment
+          ).ctx.getCachedComponent(value.type, value.key) as VaporFragment
           if (frag) return frag
         }
 

+ 1 - 0
packages/runtime-vapor/src/component.ts

@@ -588,6 +588,7 @@ export class VaporComponentInstance<
 
   // for keep-alive
   shapeFlag?: number
+  key?: any
 
   // for v-once: caches props/attrs values to ensure they remain frozen
   // even when the component re-renders due to local state changes

+ 96 - 10
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -9,6 +9,7 @@ import {
   devtoolsComponentAdded,
   getComponentName,
   isAsyncWrapper,
+  isVNode,
   matches,
   onBeforeUnmount,
   onMounted,
@@ -26,7 +27,13 @@ import {
   isVaporComponent,
 } from '../component'
 import { defineVaporComponent } from '../apiDefineComponent'
-import { ShapeFlags, invokeArrayFns, isArray } from '@vue/shared'
+import {
+  ShapeFlags,
+  invokeArrayFns,
+  isArray,
+  isFunction,
+  isObject,
+} from '@vue/shared'
 import { createElement } from '../dom/node'
 import { type VaporFragment, isDynamicFragment, isFragment } from '../fragment'
 import type { EffectScope } from '@vue/reactivity'
@@ -36,6 +43,7 @@ export interface KeepAliveContext {
   cacheBlock(): void
   cacheScope(key: any, scope: EffectScope): void
   getScope(key: any): EffectScope | undefined
+  setCurrentBranchKey(key: any): void
 }
 
 export let currentKeepAliveCtx: KeepAliveContext | null = null
@@ -59,15 +67,44 @@ export interface KeepAliveInstance extends VaporComponentInstance {
     ) => void
     deactivate: (instance: VaporComponentInstance) => void
     getCachedComponent: (
-      comp: VaporComponent,
+      comp: VaporComponent | VNode['type'] | VNode,
+      key?: any,
     ) => VaporComponentInstance | VaporFragment | undefined
     getStorageContainer: () => ParentNode
   }
 }
 
-type CacheKey = VaporComponent | VNode['type']
+type CacheKey = VaporComponent | VNode['type'] | any
 type Cache = Map<CacheKey, VaporComponentInstance | VaporFragment>
 type Keys = Set<CacheKey>
+type CompositeKey = {
+  type: VaporComponent | VNode['type']
+  branchKey: any
+}
+
+const compositeKeyCache = new WeakMap<object, Map<any, CompositeKey>>()
+const compositeKeyCachePrimitive = new Map<any, Map<any, CompositeKey>>()
+function getCompositeKey(
+  type: VaporComponent | VNode['type'],
+  branchKey: any,
+): CacheKey {
+  const isObjectType = isObject(type) || isFunction(type)
+  const perType = isObjectType
+    ? compositeKeyCache.get(type) || new Map<any, CompositeKey>()
+    : compositeKeyCachePrimitive.get(type) || new Map<any, CompositeKey>()
+  if (isObjectType) {
+    if (!compositeKeyCache.has(type)) compositeKeyCache.set(type, perType)
+  } else if (!compositeKeyCachePrimitive.has(type)) {
+    compositeKeyCachePrimitive.set(type, perType)
+  }
+
+  let composite = perType.get(branchKey)
+  if (!composite) {
+    composite = { type, branchKey }
+    perType.set(branchKey, composite)
+  }
+  return composite
+}
 
 const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
   name: 'VaporKeepAlive',
@@ -87,6 +124,9 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
     const keys: Keys = new Set()
     const storageContainer = createElement('div')
     const keptAliveScopes = new Map<any, EffectScope>()
+    // Track active keyed DynamicFragment branch key so KeepAlive can combine
+    // branch key + component type into a stable isolated cache key.
+    let currentBranchKey: any | undefined
     let current: VaporComponentInstance | VaporFragment | undefined
 
     if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
@@ -111,7 +151,12 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
 
     keepAliveInstance.ctx = {
       getStorageContainer: () => storageContainer,
-      getCachedComponent: comp => cache.get(comp),
+      getCachedComponent: (comp, key) => {
+        if (isVNode(comp)) {
+          return cache.get(resolveKey(comp.type, comp.key, currentBranchKey))
+        }
+        return cache.get(resolveKey(comp, key, currentBranchKey))
+      },
       activate: (instance, parentNode, anchor) => {
         current = instance
         activate(instance, parentNode, anchor)
@@ -163,7 +208,7 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       const [innerBlock, interop] = getInnerBlock(block)
       if (!innerBlock || !shouldCache(innerBlock, props, interop)) return
       innerCacheBlock(
-        interop ? innerBlock.vnode!.type : innerBlock.type,
+        getCacheKey(innerBlock, interop, currentBranchKey),
         innerBlock,
       )
     }
@@ -173,12 +218,14 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
       if (!innerBlock || !shouldCache(innerBlock!, props, interop)) return false
 
       if (interop) {
-        if (cache.has(innerBlock.vnode!.type)) {
+        const cacheKey = getCacheKey(innerBlock, true, currentBranchKey)
+        if (cache.has(cacheKey)) {
           innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
         }
         innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
       } else {
-        if (cache.has(innerBlock!.type)) {
+        const cacheKey = getCacheKey(innerBlock, false, currentBranchKey)
+        if (cache.has(cacheKey)) {
           innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE
         }
         innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
@@ -234,9 +281,11 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
 
         // current instance will be unmounted as part of keep-alive's unmount
         if (current) {
-          const currentKey = isVaporComponent(current)
-            ? current.type
-            : current.vnode!.type
+          const currentKey = getCacheKey(
+            current,
+            !isVaporComponent(current),
+            currentBranchKey,
+          )
           if (currentKey === key) {
             // call deactivated hook
             const da = instance.da
@@ -264,6 +313,9 @@ const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
           return scope
         }
       },
+      setCurrentBranchKey(key) {
+        currentBranchKey = key
+      },
     }
 
     const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
@@ -328,6 +380,40 @@ type InnerBlockResult =
   | [VaporComponentInstance, false]
   | [undefined, false]
 
+function resolveKey(
+  type: VaporComponent | VNode['type'],
+  key?: any,
+  branchKey?: any,
+): CacheKey {
+  if (key != null) {
+    if (branchKey !== undefined && key === branchKey) {
+      return getCompositeKey(type, branchKey)
+    }
+    return key as CacheKey
+  }
+  if (branchKey !== undefined) {
+    return getCompositeKey(type, branchKey)
+  }
+  return type as CacheKey
+}
+
+function getCacheKey(
+  block: VaporComponentInstance | VaporFragment,
+  interop: boolean,
+  branchKey?: any,
+): CacheKey {
+  if (interop) {
+    const frag = block as VaporFragment
+    return resolveKey(
+      frag.vnode!.type,
+      frag.$key !== undefined ? frag.$key : frag.vnode!.key,
+      branchKey,
+    )
+  }
+  const instance = block as VaporComponentInstance
+  return resolveKey(instance.type, instance.key, branchKey)
+}
+
 function getInnerBlock(block: Block): InnerBlockResult {
   if (isVaporComponent(block)) {
     return [block, false]

+ 6 - 1
packages/runtime-vapor/src/fragment.ts

@@ -199,6 +199,9 @@ export class DynamicFragment extends VaporFragment {
       const prevOwner = setCurrentSlotOwner(this.slotOwner)
       // set currentKeepAliveCtx so nested DynamicFragments and components can capture it
       const prevCtx = setCurrentKeepAliveCtx(keepAliveCtx)
+      if (keepAliveCtx && this.keyed) {
+        keepAliveCtx.setCurrentBranchKey(this.current)
+      }
       // switch current instance to parent instance during update
       // ensure that the parent instance is correct for nested components
       const prev = parent && instance ? setCurrentInstance(instance) : undefined
@@ -207,7 +210,7 @@ export class DynamicFragment extends VaporFragment {
       setCurrentKeepAliveCtx(prevCtx)
       setCurrentSlotOwner(prevOwner)
 
-      // set key on nodes
+      // set key on blocks
       if (this.keyed) setKey(this.nodes, this.current)
 
       if (transition) {
@@ -485,12 +488,14 @@ function setKey(block: Block & { $key?: any }, key: any) {
   if (block instanceof Node) {
     block.$key = key
   } else if (isVaporComponent(block)) {
+    block.key = key
     setKey(block.block, key)
   } else if (isArray(block)) {
     for (const b of block) {
       setKey(b, key)
     }
   } else {
+    block.$key = key
     setKey(block.nodes, key)
   }
 }