mirror of https://github.com/vuejs/core.git
feat(reactivity/watch): add pause/resume for ReactiveEffect, EffectScope, and WatchHandle (#9651)
This commit is contained in:
parent
55acabe88c
commit
267093c314
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export enum EffectFlags {
|
|||
DIRTY = 1 << 4,
|
||||
ALLOW_RECURSE = 1 << 5,
|
||||
NO_BATCH = 1 << 6,
|
||||
PAUSED = 1 << 7,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -107,6 +108,8 @@ export interface Link {
|
|||
prevActiveLink?: Link
|
||||
}
|
||||
|
||||
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
|
||||
|
||||
export class ReactiveEffect<T = any>
|
||||
implements Subscriber, ReactiveEffectOptions
|
||||
{
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export class EffectScope {
|
|||
*/
|
||||
cleanups: (() => void)[] = []
|
||||
|
||||
private _isPaused = false
|
||||
|
||||
/**
|
||||
* only assigned by undetached scope
|
||||
* @internal
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run<T>(fn: () => T): T | undefined {
|
||||
if (this._active) {
|
||||
const currentEffectScope = activeEffectScope
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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<
|
||||
|
|
@ -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<
|
||||
|
|
@ -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<
|
||||
|
|
@ -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. ` +
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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('.')
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ export type {
|
|||
WatchOptionsBase,
|
||||
WatchCallback,
|
||||
WatchSource,
|
||||
WatchHandle,
|
||||
WatchStopHandle,
|
||||
} from './apiWatch'
|
||||
export type { InjectionKey } from './apiInject'
|
||||
|
|
|
|||
Loading…
Reference in New Issue