Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reactivity): add pause/resume methods to ReactiveEffect #9651

Merged
merged 46 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5a279be
feat: add `pause/resume` function to `ReactiveEffect`
Alfred-Skyblue Sep 13, 2023
9bda252
chore: rollback
Alfred-Skyblue Sep 20, 2023
c2b7333
feat: extend `ReactiveEffect` using derived class `RenderEffect`
Alfred-Skyblue Sep 20, 2023
df77be4
Merge branch 'main' into feat-effect
Alfred-Skyblue Sep 25, 2023
a5446a3
Merge branch 'main' into feat-effect
Alfred-Skyblue Oct 17, 2023
540e408
Merge branch 'main' into feat-effect
Alfred-Skyblue Oct 19, 2023
a4e43a8
Merge branch 'main' into feat-effect
Alfred-Skyblue Oct 23, 2023
ac2dcd6
Merge branch 'main' into feat-effect
Alfred-Skyblue Oct 23, 2023
b91a0a1
Merge branch 'main' into feat-effect
Alfred-Skyblue Nov 2, 2023
92419a2
Merge branch 'main' into feat-effect
Alfred-Skyblue Nov 7, 2023
34e569c
feat(KeepAlive.ts): use the `lazy` prop to control updates
Alfred-Skyblue Nov 13, 2023
88a6ac6
test: update unit test
Alfred-Skyblue Nov 13, 2023
1c5b52d
Merge branch 'main' into feat-effect
Alfred-Skyblue Nov 14, 2023
55b2ca5
feat(reactivity): EffectScope adds pause and resume function
Alfred-Skyblue Nov 16, 2023
7ed2b07
feat(runtime-core): mark effectScope in component as not detached
Alfred-Skyblue Nov 16, 2023
cb4faf8
types(effectScope): add constructor overload
Alfred-Skyblue Nov 16, 2023
6ffcc0b
docs: update comments
Alfred-Skyblue Nov 21, 2023
930fa3e
test: add unit test
Alfred-Skyblue Nov 22, 2023
0a8f82e
Merge branch 'main' into feat/effect/pause-resume
Alfred-Skyblue Nov 28, 2023
d8754f3
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Nov 30, 2023
b0e64fe
chore: rollback component EffectScope
Alfred-Skyblue Dec 1, 2023
791c904
chore: rollback keepAlive
Alfred-Skyblue Dec 1, 2023
f2de749
feat: Intercept `scheduler` execution
Alfred-Skyblue Dec 1, 2023
7a4c366
feat(apiWatch): add pause and resume methods to WatchHandle
Alfred-Skyblue Dec 1, 2023
22cfc8e
test: update unit test
Alfred-Skyblue Dec 2, 2023
381e5f2
feat(apiWatch): add the `immediate` parameter for the `watch` functio…
Alfred-Skyblue Dec 2, 2023
baa4d4d
chore: rename attribute name
Alfred-Skyblue Dec 2, 2023
214e77f
chore: simplify effect.ts
Alfred-Skyblue Dec 4, 2023
14f0919
feat: do not allow calling the resume method after stopping
Alfred-Skyblue Dec 4, 2023
1c3facf
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Dec 5, 2023
401418c
chore: rollback renderer.ts
Alfred-Skyblue Dec 5, 2023
f782c6b
chore: rollback KeepAlive.ts
Alfred-Skyblue Dec 5, 2023
9e7c1e5
chore(effectScope): remove the `parent` parameter from EffectScope
Alfred-Skyblue Dec 5, 2023
287c182
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Dec 5, 2023
406268d
feat: `effect` is always fired once on recovery
Alfred-Skyblue Dec 6, 2023
98cfd71
chore: in SSR, when not in sync mode, directly return an NOOP
Alfred-Skyblue Dec 6, 2023
4872137
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Dec 12, 2023
4b9026a
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Dec 21, 2023
fde852b
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Dec 28, 2023
bbfeb9d
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Jan 3, 2024
ef2ad1b
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Jan 11, 2024
a1426d5
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Feb 18, 2024
108eb94
Merge branch 'minor' into feat/effect/pause-resume
Alfred-Skyblue Mar 6, 2024
272fb51
chore: Merge branch 'minor' into feat/effect/pause-resume
yyx990803 Aug 2, 2024
e2e6eae
refactor: simplify some code
yyx990803 Aug 2, 2024
d836595
chore: more simplifications
yyx990803 Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/reactivity/__tests__/effect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1282,4 +1282,48 @@ describe('reactivity/effect', () => {
).not.toHaveBeenWarned()
})
})

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(2)
expect(obj.foo).toBe(2)

obj.foo++
expect(fnSpy).toHaveBeenCalledTimes(3)
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)

obj.foo++
expect(fnSpy).toHaveBeenCalledTimes(1)
expect(obj.foo).toBe(3)

runner.effect.resume()
expect(fnSpy).toHaveBeenCalledTimes(2)
expect(obj.foo).toBe(3)
})
})
27 changes: 27 additions & 0 deletions packages/reactivity/__tests__/effectScope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,31 @@ 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)

counter.num++
await nextTick()
expect(fnSpy).toHaveBeenCalledTimes(2)

scope.resume()
expect(fnSpy).toHaveBeenCalledTimes(3)
})
})
21 changes: 20 additions & 1 deletion packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export enum EffectFlags {
DIRTY = 1 << 4,
ALLOW_RECURSE = 1 << 5,
NO_BATCH = 1 << 6,
PAUSED = 1 << 7,
}

/**
Expand Down Expand Up @@ -107,6 +108,8 @@ export interface Link {
prevActiveLink?: Link
}

const pausedQueueEffects = new WeakSet<ReactiveEffect>()

export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
Expand Down Expand Up @@ -142,6 +145,20 @@ export class ReactiveEffect<T = any>
}
}

pause() {
this.flags |= EffectFlags.PAUSED
}

resume() {
if (this.flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this)
this.trigger()
}
}
}

/**
* @internal
*/
Expand Down Expand Up @@ -207,7 +224,9 @@ export class ReactiveEffect<T = any>
}

trigger() {
if (this.scheduler) {
if (this.flags & EffectFlags.PAUSED) {
pausedQueueEffects.add(this)
} else if (this.scheduler) {
this.scheduler()
} else {
this.runIfDirty()
Expand Down
35 changes: 35 additions & 0 deletions packages/reactivity/src/effectScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export class EffectScope {
*/
cleanups: (() => void)[] = []

private _isPaused = false

/**
* only assigned by undetached scope
* @internal
Expand Down Expand Up @@ -48,6 +50,39 @@ 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()
}
Alfred-Skyblue marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Resumes the effect scope, including all child scopes and effects.
*/
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()
}
}
for (let i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].resume()
}
}
}
}
Alfred-Skyblue marked this conversation as resolved.
Show resolved Hide resolved

run<T>(fn: () => T): T | undefined {
if (this._active) {
const currentEffectScope = activeEffectScope
Expand Down
39 changes: 39 additions & 0 deletions packages/runtime-core/__tests__/apiWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1621,6 +1621,45 @@ describe('api: watch', () => {
expect(cb).toHaveBeenCalledTimes(4)
})

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).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))

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()
await nextTick()
expect(cb).toHaveBeenCalledTimes(4)
expect(cb).toHaveBeenLastCalledWith(5, 4, expect.any(Function))
})

it('shallowReactive', async () => {
const state = shallowReactive({
msg: ref('hello'),
Expand Down
40 changes: 27 additions & 13 deletions packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,17 @@ export interface WatchOptions<Immediate = boolean> 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)
}

Expand Down Expand Up @@ -119,7 +125,7 @@ export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
): WatchHandle

// overload: reactive array or tuple of multiple sources + cb
export function watch<
Expand All @@ -131,7 +137,7 @@ export function watch<
? WatchCallback<T, MaybeUndefined<T, Immediate>>
: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
): WatchHandle

// overload: array of multiple sources + cb
export function watch<
Expand All @@ -141,7 +147,7 @@ export function watch<
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
): WatchHandle

// overload: watching reactive object w/ cb
export function watch<
Expand All @@ -151,14 +157,14 @@ export function watch<
source: T,
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
options?: WatchOptions<Immediate>,
): WatchStopHandle
): WatchHandle

// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>,
): WatchStopHandle {
): WatchHandle {
if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
Expand All @@ -180,12 +186,12 @@ function doWatch(
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
): WatchHandle {
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
unwatch()
watchHandle()
}
}

Expand Down Expand Up @@ -327,7 +333,11 @@ function doWatch(
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else {
return NOOP
const watchHandle: WatchHandle = () => {}
watchHandle.stop = NOOP
watchHandle.resume = NOOP
watchHandle.pause = NOOP
return watchHandle
}
}

Expand Down Expand Up @@ -397,13 +407,17 @@ function doWatch(
effect.scheduler = scheduler

const scope = getCurrentScope()
const unwatch = () => {
const watchHandle: WatchHandle = () => {
effect.stop()
if (scope) {
remove(scope.effects, effect)
}
}

watchHandle.pause = effect.pause.bind(effect)
watchHandle.resume = effect.resume.bind(effect)
watchHandle.stop = watchHandle

if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
Expand All @@ -425,8 +439,8 @@ function doWatch(
effect.run()
}

if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
return watchHandle
}

// this.$watch
Expand All @@ -435,7 +449,7 @@ export function instanceWatch(
source: string | Function,
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions,
): WatchStopHandle {
): WatchHandle {
const publicThis = this.proxy as any
const getter = isString(source)
? source.includes('.')
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export type {
WatchOptionsBase,
WatchCallback,
WatchSource,
WatchHandle,
WatchStopHandle,
} from './apiWatch'
export type { InjectionKey } from './apiInject'
Expand Down