| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- import { nextTick, watch, watchEffect } from '@vue/runtime-core'
- import {
- type ComputedRef,
- EffectScope,
- ReactiveEffect,
- computed,
- effect,
- effectScope,
- getCurrentScope,
- onScopeDispose,
- reactive,
- ref,
- setCurrentScope,
- } from '../src'
- describe('reactivity/effect/scope', () => {
- it('should run', () => {
- const fnSpy = vi.fn(() => {})
- effectScope().run(fnSpy)
- expect(fnSpy).toHaveBeenCalledTimes(1)
- })
- it('should accept zero argument', () => {
- const scope = effectScope()
- expect(getEffectsCount(scope)).toBe(0)
- })
- it('should return run value', () => {
- expect(effectScope().run(() => 1)).toBe(1)
- })
- it('should work w/ active property', () => {
- const scope = effectScope()
- const src = computed(() => 1)
- scope.run(() => src.value)
- expect(scope.active).toBe(true)
- scope.stop()
- expect(scope.active).toBe(false)
- })
- it('should collect the effects', () => {
- const scope = effectScope()
- scope.run(() => {
- let dummy
- const counter = reactive({ num: 0 })
- effect(() => (dummy = counter.num))
- expect(dummy).toBe(0)
- counter.num = 7
- expect(dummy).toBe(7)
- })
- expect(getEffectsCount(scope)).toBe(1)
- })
- it('stop', () => {
- let dummy, doubled
- const counter = reactive({ num: 0 })
- const scope = effectScope()
- scope.run(() => {
- effect(() => (dummy = counter.num))
- effect(() => (doubled = counter.num * 2))
- })
- expect(getEffectsCount(scope)).toBe(2)
- expect(dummy).toBe(0)
- counter.num = 7
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- scope.stop()
- counter.num = 6
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- })
- it('should collect nested scope', () => {
- let dummy, doubled
- const counter = reactive({ num: 0 })
- const scope = effectScope()
- scope.run(() => {
- effect(() => (dummy = counter.num))
- // nested scope
- effectScope().run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- })
- expect(getEffectsCount(scope)).toBe(1)
- expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
- expect(dummy).toBe(0)
- counter.num = 7
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- // stop the nested scope as well
- scope.stop()
- counter.num = 6
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- })
- it('nested scope can be escaped', () => {
- let dummy, doubled
- const counter = reactive({ num: 0 })
- const scope = effectScope()
- scope.run(() => {
- effect(() => (dummy = counter.num))
- // nested scope
- effectScope(true).run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- })
- expect(getEffectsCount(scope)).toBe(1)
- expect(dummy).toBe(0)
- counter.num = 7
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- scope.stop()
- counter.num = 6
- expect(dummy).toBe(7)
- // nested scope should not be stopped
- expect(doubled).toBe(12)
- })
- it('able to run the scope', () => {
- let dummy, doubled
- const counter = reactive({ num: 0 })
- const scope = effectScope()
- scope.run(() => {
- effect(() => (dummy = counter.num))
- })
- expect(getEffectsCount(scope)).toBe(1)
- scope.run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- expect(getEffectsCount(scope)).toBe(2)
- counter.num = 7
- expect(dummy).toBe(7)
- expect(doubled).toBe(14)
- scope.stop()
- })
- it('can not run an inactive scope', () => {
- let dummy, doubled
- const counter = reactive({ num: 0 })
- const scope = effectScope()
- scope.run(() => {
- effect(() => (dummy = counter.num))
- })
- expect(getEffectsCount(scope)).toBe(1)
- scope.stop()
- expect(getEffectsCount(scope)).toBe(0)
- scope.run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- expect(getEffectsCount(scope)).toBe(1)
- counter.num = 7
- expect(dummy).toBe(0)
- expect(doubled).toBe(14)
- })
- it('should fire onScopeDispose hook', () => {
- let dummy = 0
- const scope = effectScope()
- scope.run(() => {
- onScopeDispose(() => (dummy += 1))
- onScopeDispose(() => (dummy += 2))
- })
- scope.run(() => {
- onScopeDispose(() => (dummy += 4))
- })
- expect(dummy).toBe(0)
- scope.stop()
- expect(dummy).toBe(7)
- })
- it('should warn onScopeDispose() is called when there is no active effect scope', () => {
- const spy = vi.fn()
- const scope = effectScope()
- scope.run(() => {
- onScopeDispose(spy)
- })
- expect(spy).toHaveBeenCalledTimes(0)
- onScopeDispose(spy)
- expect(
- '[Vue warn] onScopeDispose() is called when there is no active effect scope to be associated with.',
- ).toHaveBeenWarned()
- scope.stop()
- expect(spy).toHaveBeenCalledTimes(1)
- })
- it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
- const parent = effectScope()
- const child = parent.run(() => effectScope())!
- expect(parent.deps?.dep).toBe(child)
- child.stop()
- expect(parent.deps).toBeUndefined()
- })
- it('test with higher level APIs', async () => {
- const r = ref(1)
- const computedSpy = vi.fn()
- const watchSpy = vi.fn()
- const watchEffectSpy = vi.fn()
- let c: ComputedRef
- const scope = effectScope()
- scope.run(() => {
- c = computed(() => {
- computedSpy()
- return r.value + 1
- })
- watch(r, watchSpy)
- watchEffect(() => {
- watchEffectSpy()
- r.value
- c.value
- })
- })
- expect(computedSpy).toHaveBeenCalledTimes(1)
- expect(watchSpy).toHaveBeenCalledTimes(0)
- expect(watchEffectSpy).toHaveBeenCalledTimes(1)
- r.value++
- await nextTick()
- expect(computedSpy).toHaveBeenCalledTimes(2)
- expect(watchSpy).toHaveBeenCalledTimes(1)
- expect(watchEffectSpy).toHaveBeenCalledTimes(2)
- scope.stop()
- r.value++
- await nextTick()
- // should not trigger anymore
- expect(computedSpy).toHaveBeenCalledTimes(2)
- expect(watchSpy).toHaveBeenCalledTimes(1)
- expect(watchEffectSpy).toHaveBeenCalledTimes(2)
- })
- it('getCurrentScope() stays valid when running a detached nested EffectScope', () => {
- const parentScope = effectScope()
- parentScope.run(() => {
- const currentScope = getCurrentScope()
- expect(currentScope).toBeDefined()
- const detachedScope = effectScope(true)
- detachedScope.run(() => {})
- expect(getCurrentScope()).toBe(currentScope)
- })
- })
- it('calling .off() of a detached scope inside an active scope should not break currentScope', () => {
- const parentScope = effectScope()
- parentScope.run(() => {
- const childScope = effectScope(true)
- setCurrentScope(setCurrentScope(childScope))
- 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)
- })
- test('removing a watcher while stopping its effectScope', async () => {
- const count = ref(0)
- const scope = effectScope()
- let watcherCalls = 0
- let cleanupCalls = 0
- scope.run(() => {
- const stop1 = watch(count, () => {
- watcherCalls++
- })
- watch(count, (val, old, onCleanup) => {
- watcherCalls++
- onCleanup(() => {
- cleanupCalls++
- stop1()
- })
- })
- watch(count, () => {
- watcherCalls++
- })
- })
- expect(watcherCalls).toBe(0)
- expect(cleanupCalls).toBe(0)
- count.value++
- await nextTick()
- expect(watcherCalls).toBe(3)
- expect(cleanupCalls).toBe(0)
- scope.stop()
- count.value++
- await nextTick()
- expect(watcherCalls).toBe(3)
- expect(cleanupCalls).toBe(1)
- expect(getEffectsCount(scope)).toBe(0)
- expect(scope.cleanupsLength).toBe(0)
- })
- })
- function getEffectsCount(scope: EffectScope): number {
- let n = 0
- for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
- if (dep.dep instanceof ReactiveEffect) {
- n++
- }
- }
- return n
- }
|