| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import { nextTick, watch, watchEffect } from '@vue/runtime-core'
- import {
- type ComputedRef,
- EffectScope,
- computed,
- effect,
- effectScope,
- getCurrentScope,
- onScopeDispose,
- reactive,
- ref,
- } 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(scope.effects.length).toBe(0)
- })
- it('should return run value', () => {
- expect(effectScope().run(() => 1)).toBe(1)
- })
- it('should work w/ active property', () => {
- const scope = effectScope()
- scope.run(() => 1)
- 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(scope.effects.length).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(scope.effects.length).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(scope.effects.length).toBe(1)
- expect(scope.scopes!.length).toBe(1)
- expect(scope.scopes![0]).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(scope.effects.length).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(scope.effects.length).toBe(1)
- scope.run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- expect(scope.effects.length).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(scope.effects.length).toBe(1)
- scope.stop()
- scope.run(() => {
- effect(() => (doubled = counter.num * 2))
- })
- expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
- expect(scope.effects.length).toBe(1)
- counter.num = 7
- expect(dummy).toBe(0)
- expect(doubled).toBe(undefined)
- })
- 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.scopes!.includes(child)).toBe(true)
- child.stop()
- expect(parent.scopes!.includes(child)).toBe(false)
- })
- 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 // computed is lazy so trigger collection
- expect(computedSpy).toHaveBeenCalledTimes(1)
- expect(watchSpy).toHaveBeenCalledTimes(0)
- expect(watchEffectSpy).toHaveBeenCalledTimes(1)
- r.value++
- c!.value
- await nextTick()
- expect(computedSpy).toHaveBeenCalledTimes(2)
- expect(watchSpy).toHaveBeenCalledTimes(1)
- expect(watchEffectSpy).toHaveBeenCalledTimes(2)
- scope.stop()
- r.value++
- c!.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)
- childScope.on()
- childScope.off()
- expect(getCurrentScope()).toBe(parentScope)
- })
- })
- })
|