Przeglądaj źródła

fix(runtime-core): do not fire mount/activated hooks if unmounted before mounted (#9370)

close #8898
close #9264
close #9617
edison 1 rok temu
rodzic
commit
aa156ed5c4

+ 58 - 0
packages/runtime-core/__tests__/apiLifecycle.spec.ts

@@ -1,8 +1,10 @@
 import {
+  KeepAlive,
   TrackOpTypes,
   h,
   nextTick,
   nodeOps,
+  onActivated,
   onBeforeMount,
   onBeforeUnmount,
   onBeforeUpdate,
@@ -407,4 +409,60 @@ describe('api: lifecycle hooks', () => {
     await nextTick()
     expect(fn).toHaveBeenCalledTimes(4)
   })
+
+  it('immediately trigger unmount during rendering', async () => {
+    const fn = vi.fn()
+    const toggle = ref(false)
+
+    const Child = {
+      setup() {
+        onMounted(fn)
+        // trigger unmount immediately
+        toggle.value = false
+        return () => h('div')
+      },
+    }
+
+    const Comp = {
+      setup() {
+        return () => (toggle.value ? [h(Child)] : null)
+      },
+    }
+
+    render(h(Comp), nodeOps.createElement('div'))
+
+    toggle.value = true
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(0)
+  })
+
+  it('immediately trigger unmount during rendering(with KeepAlive)', async () => {
+    const mountedSpy = vi.fn()
+    const activeSpy = vi.fn()
+    const toggle = ref(false)
+
+    const Child = {
+      setup() {
+        onMounted(mountedSpy)
+        onActivated(activeSpy)
+
+        // trigger unmount immediately
+        toggle.value = false
+        return () => h('div')
+      },
+    }
+
+    const Comp = {
+      setup() {
+        return () => h(KeepAlive, [toggle.value ? h(Child) : null])
+      },
+    }
+
+    render(h(Comp), nodeOps.createElement('div'))
+
+    toggle.value = true
+    await nextTick()
+    expect(mountedSpy).toHaveBeenCalledTimes(0)
+    expect(activeSpy).toHaveBeenCalledTimes(0)
+  })
 })

+ 0 - 3
packages/runtime-core/src/apiLifecycle.ts

@@ -31,9 +31,6 @@ export function injectHook(
     const wrappedHook =
       hook.__weh ||
       (hook.__weh = (...args: unknown[]) => {
-        if (target.isUnmounted) {
-          return
-        }
         // disable tracking inside all lifecycle hooks
         // since they can potentially be called inside effects.
         pauseTracking()

+ 1 - 1
packages/runtime-core/src/component.ts

@@ -223,7 +223,7 @@ export type Component<
 
 export type { ComponentOptions }
 
-type LifecycleHook<TFn = Function> = TFn[] | null
+export type LifecycleHook<TFn = Function> = (TFn & SchedulerJob)[] | null
 
 // use `E extends any` to force evaluating type to fix #2362
 export type SetupContext<

+ 4 - 0
packages/runtime-core/src/components/KeepAlive.ts

@@ -38,6 +38,7 @@ import {
   type RendererElement,
   type RendererInternals,
   type RendererNode,
+  invalidateMount,
   queuePostRenderEffect,
 } from '../renderer'
 import { setTransitionHooks } from './BaseTransition'
@@ -166,6 +167,9 @@ const KeepAliveImpl: ComponentOptions = {
 
     sharedContext.deactivate = (vnode: VNode) => {
       const instance = vnode.component!
+      invalidateMount(instance.m)
+      invalidateMount(instance.a)
+
       move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
       queuePostRenderEffect(() => {
         if (instance.da) {

+ 10 - 1
packages/runtime-core/src/renderer.ts

@@ -17,6 +17,7 @@ import {
   type ComponentInternalInstance,
   type ComponentOptions,
   type Data,
+  type LifecycleHook,
   createComponentInstance,
   setupComponent,
 } from './component'
@@ -2266,7 +2267,9 @@ function baseCreateRenderer(
       unregisterHMR(instance)
     }
 
-    const { bum, scope, update, subTree, um } = instance
+    const { bum, scope, update, subTree, um, m, a } = instance
+    invalidateMount(m)
+    invalidateMount(a)
 
     // beforeUnmount hook
     if (bum) {
@@ -2533,3 +2536,9 @@ function locateNonHydratedAsyncRoot(
     }
   }
 }
+
+export function invalidateMount(hooks: LifecycleHook) {
+  if (hooks) {
+    for (let i = 0; i < hooks.length; i++) hooks[i].active = false
+  }
+}

+ 3 - 5
packages/runtime-core/src/scheduler.ts

@@ -185,13 +185,11 @@ export function flushPostFlushCbs(seen?: CountMap) {
       postFlushIndex < activePostFlushCbs.length;
       postFlushIndex++
     ) {
-      if (
-        __DEV__ &&
-        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
-      ) {
+      const cb = activePostFlushCbs[postFlushIndex]
+      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
         continue
       }
-      activePostFlushCbs[postFlushIndex]()
+      if (cb.active !== false) cb()
     }
     activePostFlushCbs = null
     postFlushIndex = 0