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()
|
).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)
|
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,
|
DIRTY = 1 << 4,
|
||||||
ALLOW_RECURSE = 1 << 5,
|
ALLOW_RECURSE = 1 << 5,
|
||||||
NO_BATCH = 1 << 6,
|
NO_BATCH = 1 << 6,
|
||||||
|
PAUSED = 1 << 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -107,6 +108,8 @@ export interface Link {
|
||||||
prevActiveLink?: Link
|
prevActiveLink?: Link
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
|
||||||
|
|
||||||
export class ReactiveEffect<T = any>
|
export class ReactiveEffect<T = any>
|
||||||
implements Subscriber, ReactiveEffectOptions
|
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
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
@ -207,7 +224,9 @@ export class ReactiveEffect<T = any>
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger() {
|
trigger() {
|
||||||
if (this.scheduler) {
|
if (this.flags & EffectFlags.PAUSED) {
|
||||||
|
pausedQueueEffects.add(this)
|
||||||
|
} else if (this.scheduler) {
|
||||||
this.scheduler()
|
this.scheduler()
|
||||||
} else {
|
} else {
|
||||||
this.runIfDirty()
|
this.runIfDirty()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export class EffectScope {
|
||||||
*/
|
*/
|
||||||
cleanups: (() => void)[] = []
|
cleanups: (() => void)[] = []
|
||||||
|
|
||||||
|
private _isPaused = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only assigned by undetached scope
|
* only assigned by undetached scope
|
||||||
* @internal
|
* @internal
|
||||||
|
|
@ -48,6 +50,39 @@ export class EffectScope {
|
||||||
return this._active
|
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 {
|
run<T>(fn: () => T): T | undefined {
|
||||||
if (this._active) {
|
if (this._active) {
|
||||||
const currentEffectScope = activeEffectScope
|
const currentEffectScope = activeEffectScope
|
||||||
|
|
|
||||||
|
|
@ -1621,6 +1621,45 @@ describe('api: watch', () => {
|
||||||
expect(cb).toHaveBeenCalledTimes(4)
|
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 () => {
|
it('shallowReactive', async () => {
|
||||||
const state = shallowReactive({
|
const state = shallowReactive({
|
||||||
msg: ref('hello'),
|
msg: ref('hello'),
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,17 @@ export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
|
||||||
|
|
||||||
export type WatchStopHandle = () => void
|
export type WatchStopHandle = () => void
|
||||||
|
|
||||||
|
export interface WatchHandle extends WatchStopHandle {
|
||||||
|
pause: () => void
|
||||||
|
resume: () => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
// Simple effect.
|
// Simple effect.
|
||||||
export function watchEffect(
|
export function watchEffect(
|
||||||
effect: WatchEffect,
|
effect: WatchEffect,
|
||||||
options?: WatchOptionsBase,
|
options?: WatchOptionsBase,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
return doWatch(effect, null, options)
|
return doWatch(effect, null, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +125,7 @@ export function watch<T, Immediate extends Readonly<boolean> = false>(
|
||||||
source: WatchSource<T>,
|
source: WatchSource<T>,
|
||||||
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
|
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
|
||||||
options?: WatchOptions<Immediate>,
|
options?: WatchOptions<Immediate>,
|
||||||
): WatchStopHandle
|
): WatchHandle
|
||||||
|
|
||||||
// overload: reactive array or tuple of multiple sources + cb
|
// overload: reactive array or tuple of multiple sources + cb
|
||||||
export function watch<
|
export function watch<
|
||||||
|
|
@ -131,7 +137,7 @@ export function watch<
|
||||||
? WatchCallback<T, MaybeUndefined<T, Immediate>>
|
? WatchCallback<T, MaybeUndefined<T, Immediate>>
|
||||||
: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
|
: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
|
||||||
options?: WatchOptions<Immediate>,
|
options?: WatchOptions<Immediate>,
|
||||||
): WatchStopHandle
|
): WatchHandle
|
||||||
|
|
||||||
// overload: array of multiple sources + cb
|
// overload: array of multiple sources + cb
|
||||||
export function watch<
|
export function watch<
|
||||||
|
|
@ -141,7 +147,7 @@ export function watch<
|
||||||
sources: [...T],
|
sources: [...T],
|
||||||
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
|
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
|
||||||
options?: WatchOptions<Immediate>,
|
options?: WatchOptions<Immediate>,
|
||||||
): WatchStopHandle
|
): WatchHandle
|
||||||
|
|
||||||
// overload: watching reactive object w/ cb
|
// overload: watching reactive object w/ cb
|
||||||
export function watch<
|
export function watch<
|
||||||
|
|
@ -151,14 +157,14 @@ export function watch<
|
||||||
source: T,
|
source: T,
|
||||||
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
|
cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
|
||||||
options?: WatchOptions<Immediate>,
|
options?: WatchOptions<Immediate>,
|
||||||
): WatchStopHandle
|
): WatchHandle
|
||||||
|
|
||||||
// implementation
|
// implementation
|
||||||
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
|
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
|
||||||
source: T | WatchSource<T>,
|
source: T | WatchSource<T>,
|
||||||
cb: any,
|
cb: any,
|
||||||
options?: WatchOptions<Immediate>,
|
options?: WatchOptions<Immediate>,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
if (__DEV__ && !isFunction(cb)) {
|
if (__DEV__ && !isFunction(cb)) {
|
||||||
warn(
|
warn(
|
||||||
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
|
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
|
||||||
|
|
@ -180,12 +186,12 @@ function doWatch(
|
||||||
onTrack,
|
onTrack,
|
||||||
onTrigger,
|
onTrigger,
|
||||||
}: WatchOptions = EMPTY_OBJ,
|
}: WatchOptions = EMPTY_OBJ,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
if (cb && once) {
|
if (cb && once) {
|
||||||
const _cb = cb
|
const _cb = cb
|
||||||
cb = (...args) => {
|
cb = (...args) => {
|
||||||
_cb(...args)
|
_cb(...args)
|
||||||
unwatch()
|
watchHandle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,7 +333,11 @@ function doWatch(
|
||||||
const ctx = useSSRContext()!
|
const ctx = useSSRContext()!
|
||||||
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
|
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
|
||||||
} else {
|
} 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
|
effect.scheduler = scheduler
|
||||||
|
|
||||||
const scope = getCurrentScope()
|
const scope = getCurrentScope()
|
||||||
const unwatch = () => {
|
const watchHandle: WatchHandle = () => {
|
||||||
effect.stop()
|
effect.stop()
|
||||||
if (scope) {
|
if (scope) {
|
||||||
remove(scope.effects, effect)
|
remove(scope.effects, effect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchHandle.pause = effect.pause.bind(effect)
|
||||||
|
watchHandle.resume = effect.resume.bind(effect)
|
||||||
|
watchHandle.stop = watchHandle
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
effect.onTrack = onTrack
|
effect.onTrack = onTrack
|
||||||
effect.onTrigger = onTrigger
|
effect.onTrigger = onTrigger
|
||||||
|
|
@ -425,8 +439,8 @@ function doWatch(
|
||||||
effect.run()
|
effect.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
|
if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
|
||||||
return unwatch
|
return watchHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.$watch
|
// this.$watch
|
||||||
|
|
@ -435,7 +449,7 @@ export function instanceWatch(
|
||||||
source: string | Function,
|
source: string | Function,
|
||||||
value: WatchCallback | ObjectWatchOptionItem,
|
value: WatchCallback | ObjectWatchOptionItem,
|
||||||
options?: WatchOptions,
|
options?: WatchOptions,
|
||||||
): WatchStopHandle {
|
): WatchHandle {
|
||||||
const publicThis = this.proxy as any
|
const publicThis = this.proxy as any
|
||||||
const getter = isString(source)
|
const getter = isString(source)
|
||||||
? source.includes('.')
|
? source.includes('.')
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ export type {
|
||||||
WatchOptionsBase,
|
WatchOptionsBase,
|
||||||
WatchCallback,
|
WatchCallback,
|
||||||
WatchSource,
|
WatchSource,
|
||||||
|
WatchHandle,
|
||||||
WatchStopHandle,
|
WatchStopHandle,
|
||||||
} from './apiWatch'
|
} from './apiWatch'
|
||||||
export type { InjectionKey } from './apiInject'
|
export type { InjectionKey } from './apiInject'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue