Explorar el Código

fix(runtime-vapor): clear template refs on KeepAlive deactivation

daiwei hace 2 meses
padre
commit
4c5a620459

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

@@ -2351,4 +2351,62 @@ describe('VaporKeepAlive', () => {
     await nextTick()
     expect(instanceRef.value).not.toBe(null)
   })
+
+  test('should clear old ref when switching KeepAlive branches', async () => {
+    const CompA = defineVaporComponent({
+      name: 'CompA',
+      setup() {
+        return template('<div>A</div>')()
+      },
+    })
+
+    const CompB = defineVaporComponent({
+      name: 'CompB',
+      setup() {
+        return template('<div>B</div>')()
+      },
+    })
+
+    const refA = ref<any>(null)
+    const refB = ref<any>(null)
+    const toggle = ref(true)
+
+    define({
+      setup() {
+        const setRef = createTemplateRefSetter()
+        return createComponent(VaporKeepAlive, null, {
+          default: () =>
+            createIf(
+              () => toggle.value,
+              () => {
+                const comp = createComponent(CompA)
+                setRef(comp, refA)
+                return comp
+              },
+              () => {
+                const comp = createComponent(CompB)
+                setRef(comp, refB)
+                return comp
+              },
+            ),
+        })
+      },
+    }).render()
+
+    await nextTick()
+    expect(refA.value).not.toBe(null)
+    expect(refB.value).toBe(null)
+
+    // switch to CompB — refA should be cleared
+    toggle.value = false
+    await nextTick()
+    expect(refB.value).not.toBe(null)
+    expect(refA.value).toBe(null)
+
+    // switch back to CompA — refB should be cleared
+    toggle.value = true
+    await nextTick()
+    expect(refA.value).not.toBe(null)
+    expect(refB.value).toBe(null)
+  })
 })

+ 11 - 14
packages/runtime-vapor/src/apiTemplateRef.ts

@@ -33,6 +33,7 @@ import {
   isFragment,
 } from './fragment'
 import { isInteropEnabled } from './vdomInteropState'
+import { refCleanups } from './refCleanup'
 
 export type NodeRef =
   | string
@@ -51,8 +52,6 @@ export type setRefFn = (
   refKey?: string,
 ) => NodeRef | undefined
 
-const refCleanups = new WeakMap<RefEl, { fn: () => void }>()
-
 function ensureCleanup(el: RefEl): { fn: () => void } {
   let cleanupRef = refCleanups.get(el)
   if (!cleanupRef) {
@@ -245,19 +244,17 @@ function setRef(
       queuePostFlushCb(doSet, -1)
 
       ensureCleanup(el).fn = () => {
-        queuePostFlushCb(() => {
-          if (isArray(existing)) {
-            remove(existing, refValue)
-          } else if (_isString) {
-            refs[ref] = null
-            if (__DEV__ && canSetSetupRef(ref)) {
-              setupState[ref] = null
-            }
-          } else if (_isRef) {
-            if (canSetRef(ref, refKey)) ref.value = null
-            if (refKey) refs[refKey] = null
+        if (isArray(existing)) {
+          remove(existing, refValue)
+        } else if (_isString) {
+          refs[ref] = null
+          if (__DEV__ && canSetSetupRef(ref)) {
+            setupState[ref] = null
           }
-        })
+        } else if (_isRef) {
+          if (canSetRef(ref, refKey)) ref.value = null
+          if (refKey) refs[refKey] = null
+        }
       }
     } else if (__DEV__) {
       warn('Invalid template ref type:', ref, `(${typeof ref})`)

+ 5 - 0
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -38,6 +38,7 @@ import {
   isObject,
 } from '@vue/shared'
 import { createElement } from '../dom/node'
+import { unsetRef } from '../refCleanup'
 import { type VaporFragment, isDynamicFragment, isFragment } from '../fragment'
 import type { EffectScope } from '@vue/reactivity'
 import { isInteropEnabled } from '../vdomInteropState'
@@ -539,6 +540,10 @@ export function deactivate(
   instance: VaporComponentInstance,
   container: ParentNode,
 ): void {
+  // Clear refs before deactivation, matching VDOM core's unmount path
+  // which calls setRef(null) before the deactivation check.
+  unsetRef(instance)
+
   invalidateMount(instance.m)
   invalidateMount(instance.a)
 

+ 18 - 0
packages/runtime-vapor/src/refCleanup.ts

@@ -0,0 +1,18 @@
+import type { Block } from './block'
+
+/**
+ * Stores ref cleanup functions keyed by the element/component they are set on.
+ * Shared between apiTemplateRef.ts (writes) and KeepAlive deactivate (reads).
+ */
+export const refCleanups: WeakMap<Block, { fn: () => void }> = new WeakMap()
+
+/**
+ * Synchronously clear the ref for an element being deactivated by KeepAlive.
+ * In VDOM core, refs are cleared during unmount before the deactivation check.
+ * Since Vapor's KeepAlive retains scopes (skipping onScopeDispose), we need
+ * this explicit sync cleanup path.
+ */
+export function unsetRef(el: Block): void {
+  const c = refCleanups.get(el)
+  if (c) c.fn()
+}