| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- 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(0)
- 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
- })
- })
- 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)
- childScope.on()
- childScope.off()
- expect(getCurrentScope()).toBe(parentScope)
- })
- })
- it('calling .off() out of order should unlink the scope from the active chain', () => {
- const parentScope = effectScope(true)
- const firstScope = effectScope(true)
- const secondScope = effectScope(true)
- parentScope.on()
- firstScope.on()
- secondScope.on()
- firstScope.off()
- expect(getCurrentScope()).toBe(secondScope)
- secondScope.off()
- expect(getCurrentScope()).toBe(parentScope)
- parentScope.off()
- expect(getCurrentScope()).toBeUndefined()
- })
- 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(scope.effects.length).toBe(0)
- expect(scope.cleanups.length).toBe(0)
- })
- it('should still trigger updates after stopping scope stored in reactive object', () => {
- const rs = ref({
- stage: 0,
- scope: null as any,
- })
- let renderCount = 0
- effect(() => {
- renderCount++
- return rs.value.stage
- })
- const handleBegin = () => {
- const status = rs.value
- status.stage = 1
- status.scope = effectScope()
- status.scope.run(() => {
- watch([() => status.stage], () => {})
- })
- }
- const handleExit = () => {
- const status = rs.value
- status.stage = 0
- const watchScope = status.scope
- status.scope = null
- if (watchScope) {
- watchScope.stop()
- }
- }
- expect(rs.value.stage).toBe(0)
- expect(renderCount).toBe(1)
- // 1. Click begin
- handleBegin()
- expect(rs.value.stage).toBe(1)
- expect(renderCount).toBe(2)
- // 2. Click add
- rs.value.stage++
- expect(rs.value.stage).toBe(2)
- expect(renderCount).toBe(3)
- // 3. Click end
- handleExit()
- expect(rs.value.stage).toBe(0)
- expect(renderCount).toBe(4)
- handleBegin()
- expect(rs.value.stage).toBe(1)
- expect(renderCount).toBe(5)
- })
- })
|