From 5a279be2a1f2eef8cc22522f2a5457c4dfc7eb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 13 Sep 2023 22:43:07 +0800 Subject: [PATCH 01/26] feat: add `pause/resume` function to `ReactiveEffect` --- packages/reactivity/__tests__/effect.spec.ts | 39 +++++++++++++++++++ packages/reactivity/src/effect.ts | 28 +++++++++++++ .../runtime-core/src/components/KeepAlive.ts | 4 ++ 3 files changed, 71 insertions(+) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 69d24a76520..069370e41e0 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -990,5 +990,44 @@ describe('reactivity/effect', () => { expect(fnSpy).toHaveBeenCalledTimes(3) expect(has).toBe(false) }) + + test('pause execution of side effect functions', () => { + const obj = reactive({ foo: 1 }) + const fnSpy = vi.fn(() => obj.foo) + + const runner = effect(fnSpy) + + expect(fnSpy).toHaveBeenCalledTimes(1) + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(2) + + runner.effect.pause() + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(2) + + runner.effect.resume() + + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(3) + }) + + test('immediately execute the calls during the pause when resuming', () => { + const obj = reactive({ bar: 1 }) + const fnSpy = vi.fn(() => obj.bar) + + const runner = effect(fnSpy) + + expect(fnSpy).toHaveBeenCalledTimes(1) + obj.bar++ + expect(fnSpy).toHaveBeenCalledTimes(2) + + runner.effect.pause() + obj.bar++ + expect(fnSpy).toHaveBeenCalledTimes(2) + + runner.effect.resume(true) + + expect(fnSpy).toHaveBeenCalledTimes(3) + }) }) }) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index d4a34edfef4..3124ea7e98f 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -68,6 +68,16 @@ export class ReactiveEffect { * @internal */ private deferStop?: boolean + /** + * Whether to pause + * @internal + */ + private isPaused = false + /** + * Indicates whether the run method was called during the pause process + * @internal + */ + private isCalled = false onStop?: () => void // dev only @@ -83,7 +93,23 @@ export class ReactiveEffect { recordEffectScope(this, scope) } + pause() { + this.isPaused = true + } + + resume(runOnResume = false) { + this.isPaused = false + if (runOnResume && this.isCalled) { + this.run() + } + this.isCalled = false + } + run() { + if (this.isPaused) { + this.isCalled = true + return + } if (!this.active) { return this.fn() } @@ -126,6 +152,8 @@ export class ReactiveEffect { } stop() { + // Reset the paused state first when stopping + this.resume() // stopped while running itself - defer the cleanup if (activeEffect === this) { this.deferStop = true diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 8c1b6318887..cb715806216 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -127,6 +127,8 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! + // 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 +161,8 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! + // on deactivation, pause the effect of the component instance + instance.effect.pause() move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) queuePostRenderEffect(() => { if (instance.da) { From 9bda2529a1f3091bb9394df8e51bb8dfa74e1f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 20 Sep 2023 09:52:51 +0800 Subject: [PATCH 02/26] chore: rollback --- packages/reactivity/src/effect.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 3124ea7e98f..d88664da1fe 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -98,11 +98,13 @@ export class ReactiveEffect { } resume(runOnResume = false) { - this.isPaused = false - if (runOnResume && this.isCalled) { - this.run() + if (this.isPaused) { + this.isPaused = false + if (runOnResume && this.isCalled) { + this.run() + } + this.isCalled = false } - this.isCalled = false } run() { From c2b733333c21400fa72451d1ddb31db138dd9213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 20 Sep 2023 11:55:31 +0800 Subject: [PATCH 03/26] feat: extend `ReactiveEffect` using derived class `RenderEffect` --- packages/reactivity/__tests__/effect.spec.ts | 39 ---------------- packages/reactivity/src/effect.ts | 30 ------------- .../__tests__/components/KeepAlive.spec.ts | 45 +++++++++++++++++++ packages/runtime-core/src/component.ts | 6 +-- packages/runtime-core/src/renderEffect.ts | 28 ++++++++++++ packages/runtime-core/src/renderer.ts | 9 ++-- 6 files changed, 81 insertions(+), 76 deletions(-) create mode 100644 packages/runtime-core/src/renderEffect.ts diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 069370e41e0..69d24a76520 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -990,44 +990,5 @@ describe('reactivity/effect', () => { expect(fnSpy).toHaveBeenCalledTimes(3) expect(has).toBe(false) }) - - test('pause execution of side effect functions', () => { - const obj = reactive({ foo: 1 }) - const fnSpy = vi.fn(() => obj.foo) - - const runner = effect(fnSpy) - - expect(fnSpy).toHaveBeenCalledTimes(1) - obj.foo++ - expect(fnSpy).toHaveBeenCalledTimes(2) - - runner.effect.pause() - obj.foo++ - expect(fnSpy).toHaveBeenCalledTimes(2) - - runner.effect.resume() - - obj.foo++ - expect(fnSpy).toHaveBeenCalledTimes(3) - }) - - test('immediately execute the calls during the pause when resuming', () => { - const obj = reactive({ bar: 1 }) - const fnSpy = vi.fn(() => obj.bar) - - const runner = effect(fnSpy) - - expect(fnSpy).toHaveBeenCalledTimes(1) - obj.bar++ - expect(fnSpy).toHaveBeenCalledTimes(2) - - runner.effect.pause() - obj.bar++ - expect(fnSpy).toHaveBeenCalledTimes(2) - - runner.effect.resume(true) - - expect(fnSpy).toHaveBeenCalledTimes(3) - }) }) }) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index d88664da1fe..d4a34edfef4 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -68,16 +68,6 @@ export class ReactiveEffect { * @internal */ private deferStop?: boolean - /** - * Whether to pause - * @internal - */ - private isPaused = false - /** - * Indicates whether the run method was called during the pause process - * @internal - */ - private isCalled = false onStop?: () => void // dev only @@ -93,25 +83,7 @@ export class ReactiveEffect { recordEffectScope(this, scope) } - pause() { - this.isPaused = true - } - - resume(runOnResume = false) { - if (this.isPaused) { - this.isPaused = false - if (runOnResume && this.isCalled) { - this.run() - } - this.isCalled = false - } - } - run() { - if (this.isPaused) { - this.isCalled = true - return - } if (!this.active) { return this.fn() } @@ -154,8 +126,6 @@ export class ReactiveEffect { } stop() { - // Reset the paused state first when stopping - this.resume() // stopped while running itself - defer the cleanup if (activeEffect === this) { this.deferStop = true diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index ff8ea74b622..f1f0a8c4075 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, 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 57a53a39b76..0153fbc83b6 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/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 383e17fb0f5..33f68cf2b49 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' export interface Renderer { render: RootRenderFunction @@ -1543,14 +1544,14 @@ 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, () => queueJob(update), instance.scope // track it in component's effect scope )) - const update: SchedulerJob = (instance.update = () => effect.run()) + const update: SchedulerJob = (instance.update = () => effect.update()) update.id = instance.uid // allowRecurse // #1801, #2043 component render effects should allow recursive updates From 34e569c2031ac066de2d6302ed26662ba9266c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Mon, 13 Nov 2023 23:15:37 +0800 Subject: [PATCH 04/26] feat(KeepAlive.ts): use the `lazy` prop to control updates --- .../runtime-core/src/components/KeepAlive.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index cb715806216..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,8 +129,12 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! - // on activation, resume the effect of the component instance and immediately execute the call during the pause process - instance.effect.resume(true) + + 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( @@ -161,8 +167,12 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! - // on deactivation, pause the effect of the component instance - instance.effect.pause() + + 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) { From 88a6ac6dfd40a4b673ebd79e34a521f62d65f7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Mon, 13 Nov 2023 23:20:15 +0800 Subject: [PATCH 05/26] test: update unit test --- packages/runtime-core/__tests__/components/KeepAlive.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index f1f0a8c4075..7a23b8f205a 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -992,7 +992,7 @@ describe('KeepAlive', () => { const app = createApp({ setup() { return () => { - return [h(KeepAlive, h(current.value))] + return [h(KeepAlive, { lazy: true }, h(current.value))] } } }) From 55b2ca56d791c7f016856f98e211631f7d017721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Thu, 16 Nov 2023 15:50:40 +0800 Subject: [PATCH 06/26] feat(reactivity): EffectScope adds pause and resume function --- packages/reactivity/src/effect.ts | 21 ++++++++++++ packages/reactivity/src/effectScope.ts | 32 +++++++++++++++++++ packages/runtime-core/src/component.ts | 6 ++-- .../runtime-core/src/components/KeepAlive.ts | 4 +-- packages/runtime-core/src/renderEffect.ts | 28 ---------------- packages/runtime-core/src/renderer.ts | 7 ++-- 6 files changed, 61 insertions(+), 37 deletions(-) delete mode 100644 packages/runtime-core/src/renderEffect.ts diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index c982dbd0b5a..bd3c6af8fea 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -69,6 +69,10 @@ export class ReactiveEffect { */ private deferStop?: boolean + private _isPaused = false + + private _isCalled = false + onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void @@ -83,7 +87,24 @@ export class ReactiveEffect { recordEffectScope(this, scope) } + pause() { + this._isPaused = true + } + + resume(runOnce = false) { + if (this._isPaused) { + this._isPaused = false + if (this._isCalled && runOnce) { + this.run() + } + this._isCalled = false + } + } run() { + if (this._isPaused) { + this._isCalled = true + return + } if (!this.active) { return this.fn() } diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index a65c48d031b..29963978912 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -17,6 +17,8 @@ export class EffectScope { */ cleanups: (() => void)[] = [] + private _isPaused = false + /** * only assigned by undetached scope * @internal @@ -48,6 +50,36 @@ export class EffectScope { return this._active } + pause() { + if (this._active) { + this._isPaused = true + if (this.scopes) { + for (let i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause() + } + } + for (let i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause() + } + } + } + + resume(runOnce = false) { + if (this._active) { + if (this._isPaused) { + this._isPaused = false + if (this.scopes) { + for (let i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(runOnce) + } + } + for (let i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(runOnce) + } + } + } + } + run(fn: () => T): T | undefined { if (this._active) { const currentEffectScope = activeEffectScope diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 0153fbc83b6..57a53a39b76 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -8,7 +8,8 @@ import { EffectScope, markRaw, track, - TrackOpTypes + TrackOpTypes, + ReactiveEffect } from '@vue/reactivity' import { ComponentPublicInstance, @@ -78,7 +79,6 @@ 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: RenderEffect + effect: ReactiveEffect /** * 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 336f5e2742f..e8c4b3638ac 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -132,7 +132,7 @@ const KeepAliveImpl: ComponentOptions = { 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) + instance.scope.resume(true) } move(vnode, container, anchor, MoveType.ENTER, parentSuspense) @@ -170,7 +170,7 @@ const KeepAliveImpl: ComponentOptions = { if (props.lazy) { // on deactivation, pause the effect of the component instance - instance.effect.pause() + instance.scope.pause() } move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) diff --git a/packages/runtime-core/src/renderEffect.ts b/packages/runtime-core/src/renderEffect.ts deleted file mode 100644 index 15ab62c54e2..00000000000 --- a/packages/runtime-core/src/renderEffect.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 873db471feb..64f771b55a9 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 } from '@vue/reactivity' +import { pauseTracking, ReactiveEffect, resetTracking } from '@vue/reactivity' import { updateProps } from './componentProps' import { updateSlots } from './componentSlots' import { pushWarningContext, popWarningContext, warn } from './warning' @@ -72,7 +72,6 @@ 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 { @@ -1543,13 +1542,13 @@ function baseCreateRenderer( } // create render effect for rendering - const effect = (instance.effect = new RenderEffect( + const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope )) - const update: SchedulerJob = (instance.update = () => effect.update()) + const update: SchedulerJob = (instance.update = () => effect.run()) update.id = instance.uid // allowRecurse // #1801, #2043 component render effects should allow recursive updates From 7ed2b07ee062b950c4d0811404140272db14e0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Thu, 16 Nov 2023 22:03:11 +0800 Subject: [PATCH 07/26] feat(runtime-core): mark effectScope in component as not detached --- packages/runtime-core/src/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 57a53a39b76..c911c4b8a8f 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -502,7 +502,7 @@ export function createComponentInstance( subTree: null!, // will be set synchronously right after creation effect: null!, update: null!, // will be set synchronously right after creation - scope: new EffectScope(true /* detached */), + scope: new EffectScope(false, parent?.scope), render: null, proxy: null, exposed: null, From cb4faf8f4791812859a75997f19544d31f272f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Thu, 16 Nov 2023 22:05:09 +0800 Subject: [PATCH 08/26] types(effectScope): add constructor overload --- packages/reactivity/src/effectScope.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 29963978912..995aa793b65 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -36,13 +36,15 @@ export class EffectScope { */ private index: number | undefined - constructor(public detached = false) { - this.parent = activeEffectScope - if (!detached && activeEffectScope) { - this.index = - (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( - this - ) - 1 + constructor(detached: false, parent?: EffectScope | undefined) + constructor(detached?: boolean) + constructor( + public detached = false, + parent = activeEffectScope + ) { + this.parent = parent + if (!detached && parent) { + this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1 } } @@ -146,10 +148,13 @@ export class EffectScope { * corresponding {@link https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md | RFC}. * * @param detached - Can be used to create a "detached" effect scope. + * @param parent - Can be passed to explicitly set the parent scope. * @see {@link https://vuejs.org/api/reactivity-advanced.html#effectscope} */ -export function effectScope(detached?: boolean) { - return new EffectScope(detached) +export function effectScope(detached: false, parent?: EffectScope): EffectScope +export function effectScope(detached?: boolean): EffectScope +export function effectScope(detached?: boolean, parent?: EffectScope) { + return new EffectScope(detached as any, parent) } export function recordEffectScope( From 6ffcc0beffefd36076ab6f29cb5c3c65fb2a9161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Tue, 21 Nov 2023 20:48:37 +0800 Subject: [PATCH 09/26] docs: update comments --- packages/reactivity/src/effect.ts | 8 ++++++-- packages/reactivity/src/effectScope.ts | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index bd3c6af8fea..90eed754361 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -91,10 +91,14 @@ export class ReactiveEffect { this._isPaused = true } - resume(runOnce = false) { + /** + * Resumes the execution of the reactive effect. + * @param {boolean} immediate - If true, executes the saved run method immediately upon resuming. + */ + resume(immediate: boolean = false) { if (this._isPaused) { this._isPaused = false - if (this._isCalled && runOnce) { + if (this._isCalled && immediate) { this.run() } this._isCalled = false diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 995aa793b65..d1ccc6de387 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -66,17 +66,22 @@ export class EffectScope { } } - resume(runOnce = false) { + /** + * Resumes the effect scope, including all child scopes and effects. + * + * @param {boolean} immediate - If true, executes all saved run methods in effects immediately upon resuming. + */ + resume(immediate: boolean = false) { if (this._active) { if (this._isPaused) { this._isPaused = false if (this.scopes) { for (let i = 0, l = this.scopes.length; i < l; i++) { - this.scopes[i].resume(runOnce) + this.scopes[i].resume(immediate) } } for (let i = 0, l = this.effects.length; i < l; i++) { - this.effects[i].resume(runOnce) + this.effects[i].resume(immediate) } } } From 930fa3e270dab108f33f13f493b167ccec4f56d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 22 Nov 2023 19:26:12 +0800 Subject: [PATCH 10/26] test: add unit test --- packages/reactivity/__tests__/effect.spec.ts | 44 +++++++++++++++ .../reactivity/__tests__/effectScope.spec.ts | 55 +++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index e34c7b31e40..600da200ad5 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -1015,4 +1015,48 @@ describe('reactivity/effect', () => { expect(has).toBe(false) }) }) + + test('should pause/resume effect', () => { + const obj = reactive({ foo: 1 }) + const fnSpy = vi.fn(() => obj.foo) + const runner = effect(fnSpy) + + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(1) + + runner.effect.pause() + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(2) + + runner.effect.resume() + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(2) + + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(2) + expect(obj.foo).toBe(3) + }) + + test('should be executed once immediately when resume is called', () => { + const obj = reactive({ foo: 1 }) + const fnSpy = vi.fn(() => obj.foo) + const runner = effect(fnSpy) + + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(1) + + runner.effect.pause() + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(2) + + runner.effect.resume(true) + expect(fnSpy).toHaveBeenCalledTimes(2) + expect(obj.foo).toBe(2) + + obj.foo++ + expect(fnSpy).toHaveBeenCalledTimes(3) + expect(obj.foo).toBe(3) + }) }) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index b26a90a09db..94c2b12c6f9 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -296,4 +296,59 @@ describe('reactivity/effect/scope', () => { expect(getCurrentScope()).toBe(parentScope) }) }) + + it('should pause/resume EffectScope', async () => { + const counter = reactive({ num: 0 }) + const fnSpy = vi.fn(() => counter.num) + const scope = new EffectScope() + scope.run(() => { + effect(fnSpy) + }) + + expect(fnSpy).toHaveBeenCalledTimes(1) + + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(2) + + scope.pause() + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(2) + + scope.resume() + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(2) + + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(3) + }) + + it('should execute all saved run methods in effects immediately upon resuming', async () => { + const counter = reactive({ num: 0 }) + const fnSpy = vi.fn(() => counter.num) + const scope = new EffectScope() + scope.run(() => { + effect(fnSpy) + }) + + expect(fnSpy).toHaveBeenCalledTimes(1) + + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(2) + + scope.pause() + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(2) + + scope.resume(true) + expect(fnSpy).toHaveBeenCalledTimes(3) + + counter.num++ + await nextTick() + expect(fnSpy).toHaveBeenCalledTimes(4) + }) }) From b0e64fe86d774d45d14ce4112ddcc924c21a7680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Fri, 1 Dec 2023 09:46:09 +0800 Subject: [PATCH 11/26] chore: rollback component EffectScope --- packages/runtime-core/src/component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 191436e48c7..309a7eb0e22 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -502,7 +502,7 @@ export function createComponentInstance( subTree: null!, // will be set synchronously right after creation effect: null!, update: null!, // will be set synchronously right after creation - scope: new EffectScope(false, parent?.scope), + scope: new EffectScope(true /* detached */), render: null, proxy: null, exposed: null, From 791c9042988abbf94123e66bda1cfcd28a99d393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Fri, 1 Dec 2023 16:20:36 +0800 Subject: [PATCH 12/26] chore: rollback keepAlive --- .../__tests__/components/KeepAlive.spec.ts | 45 ------------------- .../runtime-core/src/components/KeepAlive.ts | 12 ----- 2 files changed, 57 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 7a23b8f205a..ff8ea74b622 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -977,49 +977,4 @@ 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/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index e8c4b3638ac..49ab2a88cc8 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -129,12 +129,6 @@ 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.scope.resume(true) - } - move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( @@ -167,12 +161,6 @@ const KeepAliveImpl: ComponentOptions = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! - - if (props.lazy) { - // on deactivation, pause the effect of the component instance - instance.scope.pause() - } - move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) queuePostRenderEffect(() => { if (instance.da) { From f2de749d16fa32cfa1b7814a6402151fab8a7536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Fri, 1 Dec 2023 16:28:38 +0800 Subject: [PATCH 13/26] feat: Intercept `scheduler` execution --- packages/reactivity/src/effect.ts | 39 ++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index d4c6ef402ec..8f276d7f27b 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -25,6 +25,12 @@ export class ReactiveEffect { active = true deps: Dep[] = [] + _isPaused = false + + _isCalled = false + + scheduler?: EffectScheduler + /** * Can be attached after creation * @internal @@ -35,10 +41,6 @@ export class ReactiveEffect { */ allowRecurse?: boolean - private _isPaused = false - - private _isCalled = false - onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void @@ -66,12 +68,28 @@ export class ReactiveEffect { */ _depsLength = 0 + /** + * Indicates the level of dirtiness for pausing activity. + * @internal + */ + _pauseDirtyLevel = DirtyLevels.NotDirty + constructor( public fn: () => T, public trigger: () => void, - public scheduler?: EffectScheduler, + scheduler?: EffectScheduler, scope?: EffectScope ) { + if (scheduler) { + this.scheduler = (...args: Parameters) => { + if (this._isPaused) { + this.setPausedDirtyLevel() + this._isCalled = true + return + } + return scheduler(...args) + } + } recordEffectScope(this, scope) } @@ -98,6 +116,10 @@ export class ReactiveEffect { this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty } + private setPausedDirtyLevel() { + this._pauseDirtyLevel = Math.max(this._dirtyLevel, this._pauseDirtyLevel) + this._dirtyLevel = DirtyLevels.NotDirty + } pause() { this._isPaused = true } @@ -110,7 +132,12 @@ export class ReactiveEffect { if (this._isPaused) { this._isPaused = false if (this._isCalled && immediate) { - this.run() + if (this.scheduler) { + this._dirtyLevel = Math.max(this._dirtyLevel, this._pauseDirtyLevel) + this.scheduler() + } else { + this.run() + } } this._isCalled = false } From 7a4c366085267d0f74e3fd1907769e8fbcfbf671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Fri, 1 Dec 2023 16:30:17 +0800 Subject: [PATCH 14/26] feat(apiWatch): add pause and resume methods to WatchHandle --- .../runtime-core/__tests__/apiWatch.spec.ts | 21 +++++++++++++ packages/runtime-core/src/apiWatch.ts | 31 +++++++++++++------ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 1bc012bb36b..b8fd342b972 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1278,4 +1278,25 @@ describe('api: watch', () => { expect(spy1).toHaveBeenCalledTimes(1) expect(spy2).toHaveBeenCalledTimes(1) }) + + test('pause / resume', async () => { + const count = ref(0) + const cb = vi.fn() + const { pause, resume } = watch(count, cb) + + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + expect(cb).toBeCalledWith(1, 0, expect.any(Function)) + + pause() + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + + resume() + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(2) + }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 5d5b8713f8c..1120bc1f732 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -80,11 +80,17 @@ export interface WatchOptions extends WatchOptionsBase { export type WatchStopHandle = () => void +export interface WatchHandle extends WatchStopHandle { + pause: () => void + resume: () => void + stop: () => void +} + // Simple effect. export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase -): WatchStopHandle { +): WatchHandle { return doWatch(effect, null, options) } @@ -123,7 +129,7 @@ export function watch< sources: [...T], cb: WatchCallback, MapSources>, options?: WatchOptions -): WatchStopHandle +): WatchHandle // overload: multiple sources w/ `as const` // watch([foo, bar] as const, () => {}) @@ -135,14 +141,14 @@ export function watch< source: T, cb: WatchCallback, MapSources>, options?: WatchOptions -): WatchStopHandle +): WatchHandle // overload: single source + cb export function watch = false>( source: WatchSource, cb: WatchCallback, options?: WatchOptions -): WatchStopHandle +): WatchHandle // overload: watching reactive object w/ cb export function watch< @@ -152,14 +158,14 @@ export function watch< source: T, cb: WatchCallback, options?: WatchOptions -): WatchStopHandle +): WatchHandle // implementation export function watch = false>( source: T | WatchSource, cb: any, options?: WatchOptions -): WatchStopHandle { +): WatchHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + @@ -174,7 +180,7 @@ function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, once, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ -): WatchStopHandle { +): WatchHandle { if (cb && once) { const _cb = cb cb = (...args) => { @@ -315,7 +321,7 @@ function doWatch( const ctx = useSSRContext()! ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) } else { - return NOOP + return NOOP as WatchHandle } } @@ -386,6 +392,11 @@ function doWatch( } } + const watchHandle: WatchHandle = () => unwatch() + watchHandle.pause = () => effect.pause() + watchHandle.resume = () => effect.resume() + watchHandle.stop = unwatch + if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger @@ -396,7 +407,7 @@ function doWatch( if (immediate) { job() } else { - oldValue = effect.run() + if (!effect._isPaused) oldValue = effect.run() } } else if (flush === 'post') { queuePostRenderEffect( @@ -408,7 +419,7 @@ function doWatch( } if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) - return unwatch + return watchHandle } // this.$watch From 22cfc8ef65f0ea4684c72610f21ef5f711d098f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Sat, 2 Dec 2023 10:09:44 +0800 Subject: [PATCH 15/26] test: update unit test --- packages/runtime-core/__tests__/apiWatch.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index b8fd342b972..6941c404e8e 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1287,16 +1287,18 @@ describe('api: watch', () => { count.value++ await nextTick() expect(cb).toHaveBeenCalledTimes(1) - expect(cb).toBeCalledWith(1, 0, expect.any(Function)) + expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function)) pause() count.value++ await nextTick() expect(cb).toHaveBeenCalledTimes(1) + expect(cb).toHaveBeenLastCalledWith(1, 0, expect.any(Function)) resume() count.value++ await nextTick() expect(cb).toHaveBeenCalledTimes(2) + expect(cb).toHaveBeenLastCalledWith(3, 1, expect.any(Function)) }) }) From 381e5f27dd8807c489020a08436f3e7bb939ea07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Sat, 2 Dec 2023 15:59:57 +0800 Subject: [PATCH 16/26] feat(apiWatch): add the `immediate` parameter for the `watch` function upon resuming --- packages/runtime-core/__tests__/apiWatch.spec.ts | 16 ++++++++++++++++ packages/runtime-core/src/apiWatch.ts | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 6941c404e8e..b66e6651e73 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1300,5 +1300,21 @@ describe('api: watch', () => { await nextTick() expect(cb).toHaveBeenCalledTimes(2) expect(cb).toHaveBeenLastCalledWith(3, 1, expect.any(Function)) + + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(3) + expect(cb).toHaveBeenLastCalledWith(4, 3, expect.any(Function)) + + pause() + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(3) + expect(cb).toHaveBeenLastCalledWith(4, 3, expect.any(Function)) + + resume(true) + await nextTick() + expect(cb).toHaveBeenCalledTimes(4) + expect(cb).toHaveBeenLastCalledWith(5, 4, expect.any(Function)) }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 1120bc1f732..6b74b54f2e7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -82,7 +82,7 @@ export type WatchStopHandle = () => void export interface WatchHandle extends WatchStopHandle { pause: () => void - resume: () => void + resume: (immediate?: boolean) => void stop: () => void } @@ -394,7 +394,7 @@ function doWatch( const watchHandle: WatchHandle = () => unwatch() watchHandle.pause = () => effect.pause() - watchHandle.resume = () => effect.resume() + watchHandle.resume = immediate => effect.resume(immediate) watchHandle.stop = unwatch if (__DEV__) { From baa4d4dcd0bab6ccb9c58d21b7f2cd9a286adeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Sat, 2 Dec 2023 21:52:07 +0800 Subject: [PATCH 17/26] chore: rename attribute name --- packages/reactivity/src/effect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 8f276d7f27b..8fc4809d394 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -72,7 +72,7 @@ export class ReactiveEffect { * Indicates the level of dirtiness for pausing activity. * @internal */ - _pauseDirtyLevel = DirtyLevels.NotDirty + _pausedDirtyLevel = DirtyLevels.NotDirty constructor( public fn: () => T, @@ -117,7 +117,7 @@ export class ReactiveEffect { } private setPausedDirtyLevel() { - this._pauseDirtyLevel = Math.max(this._dirtyLevel, this._pauseDirtyLevel) + this._pausedDirtyLevel = Math.max(this._dirtyLevel, this._pausedDirtyLevel) this._dirtyLevel = DirtyLevels.NotDirty } pause() { @@ -133,7 +133,7 @@ export class ReactiveEffect { this._isPaused = false if (this._isCalled && immediate) { if (this.scheduler) { - this._dirtyLevel = Math.max(this._dirtyLevel, this._pauseDirtyLevel) + this._dirtyLevel = Math.max(this._dirtyLevel, this._pausedDirtyLevel) this.scheduler() } else { this.run() From 214e77f8417f261121bfa9b082988d8337bf25e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Mon, 4 Dec 2023 20:52:54 +0800 Subject: [PATCH 18/26] chore: simplify effect.ts --- packages/reactivity/src/effect.ts | 62 ++++++++------------------- packages/runtime-core/src/apiWatch.ts | 2 +- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 8fc4809d394..347440bde67 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -25,12 +25,6 @@ export class ReactiveEffect { active = true deps: Dep[] = [] - _isPaused = false - - _isCalled = false - - scheduler?: EffectScheduler - /** * Can be attached after creation * @internal @@ -67,29 +61,16 @@ export class ReactiveEffect { * @internal */ _depsLength = 0 - /** - * Indicates the level of dirtiness for pausing activity. * @internal */ - _pausedDirtyLevel = DirtyLevels.NotDirty - + _isStopped = false constructor( public fn: () => T, public trigger: () => void, - scheduler?: EffectScheduler, + public scheduler?: EffectScheduler, scope?: EffectScope ) { - if (scheduler) { - this.scheduler = (...args: Parameters) => { - if (this._isPaused) { - this.setPausedDirtyLevel() - this._isCalled = true - return - } - return scheduler(...args) - } - } recordEffectScope(this, scope) } @@ -116,12 +97,8 @@ export class ReactiveEffect { this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty } - private setPausedDirtyLevel() { - this._pausedDirtyLevel = Math.max(this._dirtyLevel, this._pausedDirtyLevel) - this._dirtyLevel = DirtyLevels.NotDirty - } pause() { - this._isPaused = true + this.active = false } /** @@ -129,26 +106,19 @@ export class ReactiveEffect { * @param {boolean} immediate - If true, executes the saved run method immediately upon resuming. */ resume(immediate: boolean = false) { - if (this._isPaused) { - this._isPaused = false - if (this._isCalled && immediate) { - if (this.scheduler) { - this._dirtyLevel = Math.max(this._dirtyLevel, this._pausedDirtyLevel) - this.scheduler() - } else { - this.run() - } + this.active = true + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this) + queueEffectSchedulers.push(this.scheduler!) + if (immediate) { + pauseScheduling() + resetScheduling() } - this._isCalled = false } } run() { this._dirtyLevel = DirtyLevels.NotDirty - if (this._isPaused) { - this._isCalled = true - return - } - if (!this.active) { + if (!this.active || this._isStopped) { return this.fn() } let lastShouldTrack = shouldTrack @@ -168,11 +138,12 @@ export class ReactiveEffect { } stop() { - if (this.active) { + if (!this._isStopped) { preCleanupEffect(this) postCleanupEffect(this) this.onStop?.() this.active = false + this._isStopped = true } } } @@ -330,6 +301,7 @@ export function trackEffect( } const queueEffectSchedulers: (() => void)[] = [] +const pausedQueueEffects = new WeakSet() export function triggerEffects( dep: Dep, @@ -356,7 +328,11 @@ export function triggerEffects( } effect.trigger() if (effect.scheduler) { - queueEffectSchedulers.push(effect.scheduler) + if (!effect.active) { + pausedQueueEffects.add(effect) + } else { + queueEffectSchedulers.push(effect.scheduler) + } } } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 6b74b54f2e7..eec03736313 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -407,7 +407,7 @@ function doWatch( if (immediate) { job() } else { - if (!effect._isPaused) oldValue = effect.run() + oldValue = effect.run() } } else if (flush === 'post') { queuePostRenderEffect( From 14f09193e12e1454ebaef9e76d5e993fb4b894b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Mon, 4 Dec 2023 21:49:11 +0800 Subject: [PATCH 19/26] feat: do not allow calling the resume method after stopping --- packages/reactivity/src/effect.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 347440bde67..ed893bbd99a 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -106,13 +106,15 @@ export class ReactiveEffect { * @param {boolean} immediate - If true, executes the saved run method immediately upon resuming. */ resume(immediate: boolean = false) { - this.active = true - if (pausedQueueEffects.has(this)) { - pausedQueueEffects.delete(this) - queueEffectSchedulers.push(this.scheduler!) - if (immediate) { - pauseScheduling() - resetScheduling() + if (!this._isStopped) { + this.active = true + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this) + queueEffectSchedulers.push(this.scheduler!) + if (immediate) { + pauseScheduling() + resetScheduling() + } } } } From 401418c4c47f7ae2e10158b45e3d9592cef66ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Tue, 5 Dec 2023 14:39:44 +0800 Subject: [PATCH 20/26] chore: rollback renderer.ts --- packages/runtime-core/src/renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 06f62bc0564..fc762af3d96 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, ReactiveEffect, resetTracking } from '@vue/reactivity' +import { pauseTracking, resetTracking, ReactiveEffect } from '@vue/reactivity' import { updateProps } from './componentProps' import { updateSlots } from './componentSlots' import { pushWarningContext, popWarningContext, warn } from './warning' @@ -1542,7 +1542,7 @@ function baseCreateRenderer( } } - // create render effect for rendering + // create reactive effect for rendering const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, NOOP, From f782c6b681f67e90ba828cdc9c1ae4d049936bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Tue, 5 Dec 2023 14:41:11 +0800 Subject: [PATCH 21/26] chore: rollback KeepAlive.ts --- packages/runtime-core/src/components/KeepAlive.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 49ab2a88cc8..8c1b6318887 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -52,7 +52,6 @@ export interface KeepAliveProps { include?: MatchPattern exclude?: MatchPattern max?: number | string - lazy?: boolean } type CacheKey = string | number | symbol | ConcreteComponent @@ -85,8 +84,7 @@ const KeepAliveImpl: ComponentOptions = { props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], - max: [String, Number], - lazy: Boolean + max: [String, Number] }, setup(props: KeepAliveProps, { slots }: SetupContext) { From 9e7c1e5cfbbaaa802d3bef9cf9ea27e80dc87845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Tue, 5 Dec 2023 14:51:01 +0800 Subject: [PATCH 22/26] chore(effectScope): remove the `parent` parameter from EffectScope --- packages/reactivity/src/effectScope.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index d1ccc6de387..ace77afbdff 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -36,15 +36,13 @@ export class EffectScope { */ private index: number | undefined - constructor(detached: false, parent?: EffectScope | undefined) - constructor(detached?: boolean) - constructor( - public detached = false, - parent = activeEffectScope - ) { - this.parent = parent - if (!detached && parent) { - this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1 + constructor(public detached = false) { + this.parent = activeEffectScope + if (!detached && activeEffectScope) { + this.index = + (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1 } } @@ -153,13 +151,10 @@ export class EffectScope { * corresponding {@link https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md | RFC}. * * @param detached - Can be used to create a "detached" effect scope. - * @param parent - Can be passed to explicitly set the parent scope. * @see {@link https://vuejs.org/api/reactivity-advanced.html#effectscope} */ -export function effectScope(detached: false, parent?: EffectScope): EffectScope -export function effectScope(detached?: boolean): EffectScope -export function effectScope(detached?: boolean, parent?: EffectScope) { - return new EffectScope(detached as any, parent) +export function effectScope(detached?: boolean) { + return new EffectScope(detached) } export function recordEffectScope( From 406268d421a0f4e872d9b947311aae1619007c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 6 Dec 2023 19:00:30 +0800 Subject: [PATCH 23/26] feat: `effect` is always fired once on recovery --- packages/reactivity/__tests__/effect.spec.ts | 14 ++++----- .../reactivity/__tests__/effectScope.spec.ts | 30 +------------------ packages/reactivity/src/effect.ts | 9 ++---- packages/reactivity/src/effectScope.ts | 8 ++--- .../runtime-core/__tests__/apiWatch.spec.ts | 2 +- packages/runtime-core/src/apiWatch.ts | 6 ++-- 6 files changed, 18 insertions(+), 51 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 3b3f583f066..65e913e4835 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -1089,11 +1089,11 @@ describe('reactivity/effect', () => { expect(obj.foo).toBe(2) runner.effect.resume() - expect(fnSpy).toHaveBeenCalledTimes(1) + expect(fnSpy).toHaveBeenCalledTimes(2) expect(obj.foo).toBe(2) obj.foo++ - expect(fnSpy).toHaveBeenCalledTimes(2) + expect(fnSpy).toHaveBeenCalledTimes(3) expect(obj.foo).toBe(3) }) @@ -1110,12 +1110,12 @@ describe('reactivity/effect', () => { expect(fnSpy).toHaveBeenCalledTimes(1) expect(obj.foo).toBe(2) - runner.effect.resume(true) - expect(fnSpy).toHaveBeenCalledTimes(2) - expect(obj.foo).toBe(2) - obj.foo++ - expect(fnSpy).toHaveBeenCalledTimes(3) + expect(fnSpy).toHaveBeenCalledTimes(1) + expect(obj.foo).toBe(3) + + runner.effect.resume() + expect(fnSpy).toHaveBeenCalledTimes(2) expect(obj.foo).toBe(3) }) }) diff --git a/packages/reactivity/__tests__/effectScope.spec.ts b/packages/reactivity/__tests__/effectScope.spec.ts index 94c2b12c6f9..f45b28a2c84 100644 --- a/packages/reactivity/__tests__/effectScope.spec.ts +++ b/packages/reactivity/__tests__/effectScope.spec.ts @@ -316,39 +316,11 @@ describe('reactivity/effect/scope', () => { await nextTick() expect(fnSpy).toHaveBeenCalledTimes(2) - scope.resume() - await nextTick() - expect(fnSpy).toHaveBeenCalledTimes(2) - - counter.num++ - await nextTick() - expect(fnSpy).toHaveBeenCalledTimes(3) - }) - - it('should execute all saved run methods in effects immediately upon resuming', async () => { - const counter = reactive({ num: 0 }) - const fnSpy = vi.fn(() => counter.num) - const scope = new EffectScope() - scope.run(() => { - effect(fnSpy) - }) - - expect(fnSpy).toHaveBeenCalledTimes(1) - counter.num++ await nextTick() expect(fnSpy).toHaveBeenCalledTimes(2) - scope.pause() - counter.num++ - await nextTick() - expect(fnSpy).toHaveBeenCalledTimes(2) - - scope.resume(true) + scope.resume() expect(fnSpy).toHaveBeenCalledTimes(3) - - counter.num++ - await nextTick() - expect(fnSpy).toHaveBeenCalledTimes(4) }) }) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index ed893bbd99a..f7e4126bdf7 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -103,18 +103,15 @@ export class ReactiveEffect { /** * Resumes the execution of the reactive effect. - * @param {boolean} immediate - If true, executes the saved run method immediately upon resuming. */ - resume(immediate: boolean = false) { + resume() { if (!this._isStopped) { this.active = true if (pausedQueueEffects.has(this)) { pausedQueueEffects.delete(this) queueEffectSchedulers.push(this.scheduler!) - if (immediate) { - pauseScheduling() - resetScheduling() - } + pauseScheduling() + resetScheduling() } } } diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index ace77afbdff..e5294ce19b8 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -66,20 +66,18 @@ export class EffectScope { /** * Resumes the effect scope, including all child scopes and effects. - * - * @param {boolean} immediate - If true, executes all saved run methods in effects immediately upon resuming. */ - resume(immediate: boolean = false) { + resume() { if (this._active) { if (this._isPaused) { this._isPaused = false if (this.scopes) { for (let i = 0, l = this.scopes.length; i < l; i++) { - this.scopes[i].resume(immediate) + this.scopes[i].resume() } } for (let i = 0, l = this.effects.length; i < l; i++) { - this.effects[i].resume(immediate) + this.effects[i].resume() } } } diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index b66e6651e73..872b623a12f 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1312,7 +1312,7 @@ describe('api: watch', () => { expect(cb).toHaveBeenCalledTimes(3) expect(cb).toHaveBeenLastCalledWith(4, 3, expect.any(Function)) - resume(true) + resume() await nextTick() expect(cb).toHaveBeenCalledTimes(4) expect(cb).toHaveBeenLastCalledWith(5, 4, expect.any(Function)) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index eec03736313..83b84dc4bca 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -82,7 +82,7 @@ export type WatchStopHandle = () => void export interface WatchHandle extends WatchStopHandle { pause: () => void - resume: (immediate?: boolean) => void + resume: () => void stop: () => void } @@ -393,8 +393,8 @@ function doWatch( } const watchHandle: WatchHandle = () => unwatch() - watchHandle.pause = () => effect.pause() - watchHandle.resume = immediate => effect.resume(immediate) + watchHandle.pause = effect.pause.bind(effect) + watchHandle.resume = effect.resume.bind(effect) watchHandle.stop = unwatch if (__DEV__) { From 98cfd711bff6f1fed2e1d69dfc9ab68c2d269a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=B6=E8=BF=9C=E6=96=B9?= Date: Wed, 6 Dec 2023 19:13:22 +0800 Subject: [PATCH 24/26] chore: in SSR, when not in sync mode, directly return an NOOP --- packages/runtime-core/src/apiWatch.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 83b84dc4bca..20a32f4e873 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -321,7 +321,11 @@ function doWatch( const ctx = useSSRContext()! ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) } else { - return NOOP as WatchHandle + const watchHandle: WatchHandle = () => {} + watchHandle.stop = NOOP + watchHandle.resume = NOOP + watchHandle.pause = NOOP + return watchHandle } } From e2e6eae4c5da0f9c47435fd228632cbd578ad3e9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 2 Aug 2024 14:24:51 +0800 Subject: [PATCH 25/26] refactor: simplify some code --- packages/reactivity/src/effect.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index b63e79683bd..ad3a654169d 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -146,9 +146,7 @@ export class ReactiveEffect } pause() { - if (!(this.flags & EffectFlags.PAUSED)) { - this.flags |= EffectFlags.PAUSED - } + this.flags |= EffectFlags.PAUSED } resume() { @@ -228,9 +226,7 @@ export class ReactiveEffect trigger() { if (this.flags & EffectFlags.PAUSED) { pausedQueueEffects.add(this) - return - } - if (this.scheduler) { + } else if (this.scheduler) { this.scheduler() } else { this.runIfDirty() From d83659549c37904f035850475dfe110579d9cee3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 2 Aug 2024 14:32:58 +0800 Subject: [PATCH 26/26] chore: more simplifications --- packages/runtime-core/src/apiWatch.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 73057cd7cf6..60bc78eda31 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -191,7 +191,7 @@ function doWatch( const _cb = cb cb = (...args) => { _cb(...args) - unwatch() + watchHandle() } } @@ -407,17 +407,16 @@ function doWatch( effect.scheduler = scheduler const scope = getCurrentScope() - const unwatch = () => { + const watchHandle: WatchHandle = () => { effect.stop() if (scope) { remove(scope.effects, effect) } } - const watchHandle: WatchHandle = () => unwatch() watchHandle.pause = effect.pause.bind(effect) watchHandle.resume = effect.resume.bind(effect) - watchHandle.stop = unwatch + watchHandle.stop = watchHandle if (__DEV__) { effect.onTrack = onTrack @@ -440,7 +439,7 @@ function doWatch( effect.run() } - if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) + if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle) return watchHandle }