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

close #8898
close #9264
close #9617
This commit is contained in:
edison 2024-06-07 13:48:50 +08:00 committed by GitHub
parent 32262a9af5
commit aa156ed5c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 76 additions and 10 deletions

View File

@ -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)
})
})

View File

@ -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()

View File

@ -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<

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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