vue3-yuanma/packages/reactivity/__tests__/effectScope.spec.ts

281 lines
6.3 KiB
TypeScript
Raw Normal View History

import { nextTick, watch, watchEffect } from '@vue/runtime-core'
import {
reactive,
effect,
EffectScope,
onScopeDispose,
computed,
ref,
ComputedRef,
getCurrentScope
} from '../src'
describe('reactivity/effect/scope', () => {
it('should run', () => {
const fnSpy = jest.fn(() => {})
new EffectScope().run(fnSpy)
expect(fnSpy).toHaveBeenCalledTimes(1)
})
it('should accept zero argument', () => {
const scope = new EffectScope()
expect(scope.effects.length).toBe(0)
})
it('should return run value', () => {
expect(new EffectScope().run(() => 1)).toBe(1)
})
it('should collect the effects', () => {
const scope = new 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 = new 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 = new EffectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// nested scope
new EffectScope().run(() => {
effect(() => (doubled = counter.num * 2))
})
})
expect(scope.effects.length).toBe(1)
2021-07-29 22:45:05 +08:00
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 = new EffectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
// nested scope
new 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 stoped
expect(doubled).toBe(12)
})
it('able to run the scope', () => {
let dummy, doubled
const counter = reactive({ num: 0 })
const scope = new 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 = new 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 = new 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 = jest.fn()
const scope = new 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 derefence child scope from parent scope after stopping child scope (no memleaks)', () => {
const parent = new EffectScope()
const child = parent.run(() => new EffectScope())!
2021-07-29 22:45:05 +08:00
expect(parent.scopes!.includes(child)).toBe(true)
child.stop()
2021-07-29 22:45:05 +08:00
expect(parent.scopes!.includes(child)).toBe(false)
})
it('test with higher level APIs', async () => {
const r = ref(1)
const computedSpy = jest.fn()
const watchSpy = jest.fn()
const watchEffectSpy = jest.fn()
let c: ComputedRef
const scope = new 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 = new EffectScope()
parentScope.run(() => {
const currentScope = getCurrentScope()
expect(currentScope).toBeDefined()
const detachedScope = new EffectScope(true)
detachedScope.run(() => {})
expect(getCurrentScope()).toBe(currentScope)
})
})
})