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