| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- 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)
- })
- 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)
- })
- 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)
- })
- })
- 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
- }
|