diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index ff8ea74b622..7a23b8f205a 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -977,4 +977,49 @@ describe('KeepAlive', () => { expect(mountedB).toHaveBeenCalledTimes(1) expect(unmountedB).toHaveBeenCalledTimes(0) }) + + test('should resume/pause update in activated/deactivated', async () => { + const renderA = vi.fn(() => 'A') + const msg = ref('hello') + const A = { + render: () => h('div', [renderA(), msg.value]) + } + const B = { + render: () => 'B' + } + + const current = shallowRef(A) + const app = createApp({ + setup() { + return () => { + return [h(KeepAlive, { lazy: true }, h(current.value))] + } + } + }) + + app.mount(root) + + expect(serializeInner(root)).toBe(`
Ahello
`) + expect(renderA).toHaveBeenCalledTimes(1) + msg.value = 'world' + await nextTick() + expect(serializeInner(root)).toBe(`
Aworld
`) + expect(renderA).toHaveBeenCalledTimes(2) + + // @ts-expect-error + current.value = B + await nextTick() + expect(serializeInner(root)).toBe(`B`) + expect(renderA).toHaveBeenCalledTimes(2) + + msg.value = 'hello world' + await nextTick() + expect(serializeInner(root)).toBe(`B`) + expect(renderA).toHaveBeenCalledTimes(2) + + current.value = A + await nextTick() + expect(serializeInner(root)).toBe(`
Ahello world
`) + expect(renderA).toHaveBeenCalledTimes(3) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 309a7eb0e22..87e8ab2836a 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -8,8 +8,7 @@ import { EffectScope, markRaw, track, - TrackOpTypes, - ReactiveEffect + TrackOpTypes } from '@vue/reactivity' import { ComponentPublicInstance, @@ -79,6 +78,7 @@ import { } from './compat/compatConfig' import { SchedulerJob } from './scheduler' import { LifecycleHooks } from './enums' +import { RenderEffect } from './renderEffect' export type Data = Record @@ -240,7 +240,7 @@ export interface ComponentInternalInstance { /** * Render effect instance */ - effect: ReactiveEffect + effect: RenderEffect /** * Bound effect runner to be passed to schedulers */ diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 8c1b6318887..336f5e2742f 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -52,6 +52,7 @@ export interface KeepAliveProps { include?: MatchPattern exclude?: MatchPattern max?: number | string + lazy?: boolean } type CacheKey = string | number | symbol | ConcreteComponent @@ -84,7 +85,8 @@ const KeepAliveImpl: ComponentOptions = { props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], - max: [String, Number] + max: [String, Number], + lazy: Boolean }, setup(props: KeepAliveProps, { slots }: SetupContext) { @@ -127,6 +129,12 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! + + if (props.lazy) { + // on activation, resume the effect of the component instance and immediately execute the call during the pause process + instance.effect.resume(true) + } + move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( @@ -159,6 +167,12 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! + + if (props.lazy) { + // on deactivation, pause the effect of the component instance + instance.effect.pause() + } + move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) queuePostRenderEffect(() => { if (instance.da) { diff --git a/packages/runtime-core/src/renderEffect.ts b/packages/runtime-core/src/renderEffect.ts new file mode 100644 index 00000000000..15ab62c54e2 --- /dev/null +++ b/packages/runtime-core/src/renderEffect.ts @@ -0,0 +1,28 @@ +import { ReactiveEffect } from '@vue/reactivity' + +/** + * Extend `ReactiveEffect` by adding `pause` and `resume` methods for controlling the execution of the `render` function. + */ +export class RenderEffect extends ReactiveEffect { + private _isPaused = false + private _isCalled = false + pause() { + this._isPaused = true + } + resume(runOnce = false) { + if (this._isPaused) { + this._isPaused = false + if (this._isCalled && runOnce) { + super.run() + } + this._isCalled = false + } + } + update() { + if (this._isPaused) { + this._isCalled = true + } else { + return super.run() + } + } +} diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index fc762af3d96..32c34e18203 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -45,7 +45,7 @@ import { flushPreFlushCbs, SchedulerJob } from './scheduler' -import { pauseTracking, resetTracking, ReactiveEffect } from '@vue/reactivity' +import { pauseTracking, resetTracking } from '@vue/reactivity' import { updateProps } from './componentProps' import { updateSlots } from './componentSlots' import { pushWarningContext, popWarningContext, warn } from './warning' @@ -72,6 +72,7 @@ import { initFeatureFlags } from './featureFlags' import { isAsyncWrapper } from './apiAsyncComponent' import { isCompatEnabled } from './compat/compatConfig' import { DeprecationTypes } from './compat/compatConfig' +import { RenderEffect } from './renderEffect' import { TransitionHooks } from './components/BaseTransition' export interface Renderer { @@ -1542,8 +1543,8 @@ function baseCreateRenderer( } } - // create reactive effect for rendering - const effect = (instance.effect = new ReactiveEffect( + // create render effect for rendering + const effect = (instance.effect = new RenderEffect( componentUpdateFn, NOOP, () => queueJob(update), @@ -1552,7 +1553,7 @@ function baseCreateRenderer( const update: SchedulerJob = (instance.update = () => { if (effect.dirty) { - effect.run() + effect.update() } }) update.id = instance.uid