]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(transition): skip enter guard while hmr updating (#14611)
authoredison <daiwei521@126.com>
Wed, 25 Mar 2026 06:27:49 +0000 (14:27 +0800)
committerGitHub <noreply@github.com>
Wed, 25 Mar 2026 06:27:49 +0000 (14:27 +0800)
close #14608

packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/renderer.ts
packages/vue/__tests__/e2e/Transition.spec.ts

index 8788bb80d6a81f545b6d6a6a3ee6f86a1b50a7dd..8184395fb39694fcec4880c686e3bb2a7d9356bd 100644 (file)
@@ -21,6 +21,7 @@ import { onBeforeUnmount, onMounted } from '../apiLifecycle'
 import { isTeleport } from './Teleport'
 import type { RendererElement } from '../renderer'
 import { SchedulerJobFlags } from '../scheduler'
+import { isHmrUpdating } from '../hmr'
 
 type Hook<T = () => void> = T | T[]
 
@@ -401,7 +402,7 @@ export function resolveTransitionHooks(
 
     enter(el) {
       // prevent enter if leave is in progress
-      if (leavingVNodesCache[key] === vnode) return
+      if (!isHmrUpdating && leavingVNodesCache[key] === vnode) return
       let hook = onEnter
       let afterHook = onAfterEnter
       let cancelHook = onEnterCancelled
index 4191a34f82f32e66ab08e379534a18e2cb4fff57..18ad691b19be0f39721fd01807edc963f74c6fb5 100644 (file)
@@ -14,6 +14,14 @@ type HMRComponent = ComponentOptions | ClassComponent
 
 export let isHmrUpdating = false
 
+export const setHmrUpdating = (v: boolean): boolean => {
+  try {
+    return isHmrUpdating
+  } finally {
+    isHmrUpdating = v
+  }
+}
+
 export const hmrDirtyComponents: Map<
   ConcreteComponent,
   Set<ComponentInternalInstance>
index 51508b35fab931bc165c2cc6028baa84e6f77081..1cc28173f5aee517ac0ddd3b61c72b81fe8e7b65 100644 (file)
@@ -71,7 +71,12 @@ import {
   type TeleportVNode,
 } from './components/Teleport'
 import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
-import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
+import {
+  isHmrUpdating,
+  registerHMR,
+  setHmrUpdating,
+  unregisterHMR,
+} from './hmr'
 import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
 import { invokeDirectiveHook } from './directives'
 import { endMeasure, startMeasure } from './profiling'
@@ -733,10 +738,17 @@ function baseCreateRenderer(
       needCallTransitionHooks ||
       dirs
     ) {
+      const isHmr = __DEV__ && isHmrUpdating
       queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
-        needCallTransitionHooks && transition!.enter(el)
-        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+        let prev
+        if (__DEV__) prev = setHmrUpdating(isHmr)
+        try {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+          needCallTransitionHooks && transition!.enter(el)
+          dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+        } finally {
+          if (__DEV__) setHmrUpdating(prev!)
+        }
       }, parentSuspense)
     }
   }
index 46e9b10aff79f1319e5c14c609c685688f70e759..79ea90b196ee08c50a2b0cc4e3a003f8d420a382 100644 (file)
@@ -1655,6 +1655,75 @@ describe('e2e: Transition', () => {
       E2E_TIMEOUT,
     )
 
+    // #14608
+    test(
+      'hmr reload child wrapped in KeepAlive (out-in mode)',
+      async () => {
+        await page().evaluate(
+          async ({ duration, childId }) => {
+            const { createApp } = (window as any).Vue
+            const { createRecord } = (window as any).__VUE_HMR_RUNTIME__
+
+            const Child = {
+              __hmrId: childId,
+              name: 'OriginalChild',
+              data() {
+                return { count: 0 }
+              },
+              template: `<div class="test">{{ count }}</div>`,
+            }
+
+            createRecord(childId, Child)
+
+            createApp({
+              components: { Child },
+              data() {
+                return { toggle: true }
+              },
+              template: `
+                <div id="container">
+                  <transition name="test" mode="out-in" :duration="${duration}">
+                    <KeepAlive>
+                      <Child v-if="toggle" />
+                    </KeepAlive>
+                  </transition>
+                </div>
+              `,
+            }).mount('#app')
+
+            await (window as any).Vue.nextTick()
+          },
+          { duration, childId: 'transition-keepalive-out-in-hmr' },
+        )
+
+        expect(await html('#container')).toBe('<div class="test">0</div>')
+
+        await page().evaluate(async childId => {
+          const { reload } = (window as any).__VUE_HMR_RUNTIME__
+          reload(childId, {
+            __hmrId: childId,
+            name: 'UpdatedChild',
+            data() {
+              return { count: 1 }
+            },
+            template: `<div class="test">{{ count }}</div>`,
+          })
+
+          await (window as any).Vue.nextTick()
+        }, 'transition-keepalive-out-in-hmr')
+
+        await nextFrame()
+        expect(await html('#container')).toBe(
+          '<div class="test test-leave-active test-leave-to">0</div>' +
+            '<div class="test test-enter-active test-enter-to">1</div>',
+        )
+
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div class="test">1</div>')
+      },
+      E2E_TIMEOUT,
+    )
+
     // #12860
     test(
       'unmount children',