ソースを参照

fix(runtime-vapor): compare raw keys before transition early removal

Mirror VDOM's same-vnode guard so keys like 1 and '1' do not collide through
the String($key) leaving-cache slot. Also simplify an equivalent in-out branch
condition.
daiwei 18 時間 前
コミット
5689b88ece

+ 31 - 0
packages/runtime-vapor/__tests__/components/Transition.spec.ts

@@ -556,6 +556,37 @@ describe('Transition', () => {
     leaveDone && leaveDone()
   })
 
+  test('does not early-remove across mixed number/string keys of equal value', async () => {
+    let leaveDone: (() => void) | undefined
+    const data = ref<any>({
+      k: 1,
+      onLeave: (_el: Element, done: () => void) => {
+        leaveDone = done
+      },
+    })
+    const App = compile(
+      `<template>
+        <Transition name="t" @leave="data.onLeave">
+          <div :key="data.k">{{ data.k }}</div>
+        </Transition>
+      </template>`,
+      data,
+    )
+    const { host } = define(App as any).render()
+    await nextTick()
+
+    // 1 (number) -> '1' (string): same String($key) bucket, different raw key.
+    // The leaving number-keyed node must NOT be early-removed by the entering
+    // string-keyed node, so both coexist during the leave (matching VDOM's
+    // isSameVNodeType raw-key guard). Without the guard the leaving node is
+    // force-removed and only 1 element remains.
+    data.value.k = '1'
+    await nextTick()
+    expect(host.querySelectorAll('div').length).toBe(2)
+
+    leaveDone && leaveDone()
+  })
+
   test('interop slot fallback should participate in out-in transition swaps', async () => {
     const data = ref({
       show: false,

+ 12 - 5
packages/runtime-vapor/src/components/Transition.ts

@@ -294,10 +294,17 @@ const getTransitionHooksContext = (
     },
     earlyRemove: () => {
       const leavingNode = leavingNodes[key]
-      const el = leavingNode && getLeaveElement(leavingNode)
-      if (el && el[leaveCbKey]) {
-        // force early removal (not cancelled)
-        el[leaveCbKey]!()
+      // Mirror VDOM's isSameVNodeType raw-key guard. The type dimension is
+      // already isolated by the leaving-cache bucket, but the slot index is
+      // String($key), which coerces e.g. 1 and '1' into the same slot. Compare
+      // the raw keys so a number-keyed leaving node isn't force-removed by a
+      // string-keyed entering node (and vice versa).
+      if (leavingNode && leavingNode.$key === block.$key) {
+        const el = getLeaveElement(leavingNode)
+        if (el && el[leaveCbKey]) {
+          // force early removal (not cancelled)
+          el[leaveCbKey]!()
+        }
       }
     },
     cloneHooks: block => {
@@ -517,7 +524,7 @@ function removeBranchWithLeaveImpl(
     mode &&
     // in-out only works when there is an incoming branch to trigger
     // delayedLeave; otherwise the current branch should leave immediately.
-    (mode !== 'in-out' || (mode === 'in-out' && render)) &&
+    (mode !== 'in-out' || render) &&
     // out-in only needs to defer when the current branch actually has
     // a rendered child to leave before mounting the next one.
     (mode !== 'out-in' || isValidBlock(frag.nodes))