).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)
+ })
})
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)
+ })
})
DIRTY = 1 << 4,
ALLOW_RECURSE = 1 << 5,
NO_BATCH = 1 << 6,
+ PAUSED = 1 << 7,
}
/**
prevActiveLink?: Link
}
+const pausedQueueEffects = new WeakSet<ReactiveEffect>()
+
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
}
}
+ 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
*/
}
trigger() {
- if (this.scheduler) {
+ if (this.flags & EffectFlags.PAUSED) {
+ pausedQueueEffects.add(this)
+ } else if (this.scheduler) {
this.scheduler()
} else {
this.runIfDirty()
*/
cleanups: (() => void)[] = []
+ private _isPaused = false
+
/**
* only assigned by undetached scope
* @internal
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
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'),
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)
}
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<
? 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<
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>,
-): WatchStopHandle
+): WatchHandle
// overload: watching reactive object w/ cb
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. ` +
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ,
-): WatchStopHandle {
+): WatchHandle {
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
- unwatch()
+ watchHandle()
}
}
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
}
}
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
effect.run()
}
- if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
- return unwatch
+ if (__SSR__ && ssrCleanup) ssrCleanup.push(watchHandle)
+ return watchHandle
}
// this.$watch
source: string | Function,
value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions,
-): WatchStopHandle {
+): WatchHandle {
const publicThis = this.proxy as any
const getter = isString(source)
? source.includes('.')
WatchOptionsBase,
WatchCallback,
WatchSource,
+ WatchHandle,
WatchStopHandle,
} from './apiWatch'
export type { InjectionKey } from './apiInject'