vue3-core/packages/reactivity/__tests__/computed.spec.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

455 lines
10 KiB
TypeScript
Raw Normal View History

import {
type DebuggerEvent,
ITERATE_KEY,
TrackOpTypes,
TriggerOpTypes,
type WritableComputedRef,
computed,
effect,
2021-07-07 22:10:56 +08:00
isReadonly,
reactive,
ref,
toRaw,
} from '../src'
2018-09-21 06:03:59 +08:00
2019-08-20 21:58:10 +08:00
describe('reactivity/computed', () => {
2018-09-21 06:03:59 +08:00
it('should return updated value', () => {
const value = reactive<{ foo?: number }>({})
2018-09-21 06:03:59 +08:00
const cValue = computed(() => value.foo)
2019-05-29 18:47:29 +08:00
expect(cValue.value).toBe(undefined)
2018-09-21 06:03:59 +08:00
value.foo = 1
2019-05-29 18:47:29 +08:00
expect(cValue.value).toBe(1)
2018-09-21 06:03:59 +08:00
})
it('should compute lazily', () => {
const value = reactive<{ foo?: number }>({})
const getter = vi.fn(() => value.foo)
2018-09-21 06:03:59 +08:00
const cValue = computed(getter)
// lazy
expect(getter).not.toHaveBeenCalled()
2019-05-29 18:47:29 +08:00
expect(cValue.value).toBe(undefined)
2018-09-21 06:03:59 +08:00
expect(getter).toHaveBeenCalledTimes(1)
// should not compute again
2019-05-29 18:47:29 +08:00
cValue.value
2018-09-21 06:03:59 +08:00
expect(getter).toHaveBeenCalledTimes(1)
// should not compute until needed
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)
// now it should compute
2019-05-29 18:47:29 +08:00
expect(cValue.value).toBe(1)
2018-09-21 06:03:59 +08:00
expect(getter).toHaveBeenCalledTimes(2)
// should not compute again
2019-05-29 18:47:29 +08:00
cValue.value
2018-09-21 06:03:59 +08:00
expect(getter).toHaveBeenCalledTimes(2)
})
2018-11-14 00:03:35 +08:00
it('should trigger effect', () => {
const value = reactive<{ foo?: number }>({})
2018-09-21 06:03:59 +08:00
const cValue = computed(() => value.foo)
let dummy
2018-11-14 00:03:35 +08:00
effect(() => {
2019-05-29 18:47:29 +08:00
dummy = cValue.value
2018-09-21 06:03:59 +08:00
})
expect(dummy).toBe(undefined)
value.foo = 1
expect(dummy).toBe(1)
})
it('should work when chained', () => {
const value = reactive({ foo: 0 })
2018-09-21 06:03:59 +08:00
const c1 = computed(() => value.foo)
2019-05-29 18:47:29 +08:00
const c2 = computed(() => c1.value + 1)
expect(c2.value).toBe(1)
expect(c1.value).toBe(0)
2018-09-21 06:03:59 +08:00
value.foo++
2019-05-29 18:47:29 +08:00
expect(c2.value).toBe(2)
expect(c1.value).toBe(1)
2018-09-21 06:03:59 +08:00
})
2018-11-14 00:03:35 +08:00
it('should trigger effect when chained', () => {
const value = reactive({ foo: 0 })
const getter1 = vi.fn(() => value.foo)
const getter2 = vi.fn(() => {
2019-05-29 18:47:29 +08:00
return c1.value + 1
2018-09-21 06:03:59 +08:00
})
const c1 = computed(getter1)
const c2 = computed(getter2)
let dummy
2018-11-14 00:03:35 +08:00
effect(() => {
2019-05-29 18:47:29 +08:00
dummy = c2.value
2018-09-21 06:03:59 +08:00
})
expect(dummy).toBe(1)
expect(getter1).toHaveBeenCalledTimes(1)
expect(getter2).toHaveBeenCalledTimes(1)
value.foo++
expect(dummy).toBe(2)
// should not result in duplicate calls
expect(getter1).toHaveBeenCalledTimes(2)
expect(getter2).toHaveBeenCalledTimes(2)
})
2018-11-14 00:03:35 +08:00
it('should trigger effect when chained (mixed invocations)', () => {
const value = reactive({ foo: 0 })
const getter1 = vi.fn(() => value.foo)
const getter2 = vi.fn(() => {
2019-05-29 18:47:29 +08:00
return c1.value + 1
2018-09-21 06:03:59 +08:00
})
const c1 = computed(getter1)
const c2 = computed(getter2)
let dummy
2018-11-14 00:03:35 +08:00
effect(() => {
2019-05-29 18:47:29 +08:00
dummy = c1.value + c2.value
2018-09-21 06:03:59 +08:00
})
expect(dummy).toBe(1)
expect(getter1).toHaveBeenCalledTimes(1)
expect(getter2).toHaveBeenCalledTimes(1)
value.foo++
expect(dummy).toBe(3)
// should not result in duplicate calls
expect(getter1).toHaveBeenCalledTimes(2)
expect(getter2).toHaveBeenCalledTimes(2)
})
it('should no longer update when stopped', () => {
const value = reactive<{ foo?: number }>({})
2018-09-21 06:03:59 +08:00
const cValue = computed(() => value.foo)
let dummy
2018-11-14 00:03:35 +08:00
effect(() => {
2019-05-29 18:47:29 +08:00
dummy = cValue.value
2018-09-21 06:03:59 +08:00
})
expect(dummy).toBe(undefined)
value.foo = 1
expect(dummy).toBe(1)
cValue.effect.stop()
2018-09-21 06:03:59 +08:00
value.foo = 2
expect(dummy).toBe(1)
})
2019-06-06 15:19:04 +08:00
it('should support setter', () => {
const n = ref(1)
const plusOne = computed({
get: () => n.value + 1,
set: val => {
n.value = val - 1
},
})
expect(plusOne.value).toBe(2)
n.value++
expect(plusOne.value).toBe(3)
plusOne.value = 0
expect(n.value).toBe(-1)
})
it('should trigger effect w/ setter', () => {
const n = ref(1)
const plusOne = computed({
get: () => n.value + 1,
set: val => {
n.value = val - 1
},
})
let dummy
effect(() => {
dummy = n.value
})
expect(dummy).toBe(1)
plusOne.value = 0
expect(dummy).toBe(-1)
2019-06-06 15:19:04 +08:00
})
// #5720
it('should invalidate before non-computed effects', () => {
let plusOneValues: number[] = []
const n = ref(0)
const plusOne = computed(() => n.value + 1)
effect(() => {
n.value
plusOneValues.push(plusOne.value)
})
// access plusOne, causing it to be non-dirty
plusOne.value
// mutate n
n.value++
// on the 2nd run, plusOne.value should have already updated.
expect(plusOneValues).toMatchObject([1, 2])
})
it('should warn if trying to set a readonly computed', () => {
const n = ref(1)
const plusOne = computed(() => n.value + 1)
;(plusOne as WritableComputedRef<number>).value++ // Type cast to prevent TS from preventing the error
expect(
'Write operation failed: computed value is readonly',
).toHaveBeenWarnedLast()
})
it('should be readonly', () => {
let a = { a: 1 }
const x = computed(() => a)
expect(isReadonly(x)).toBe(true)
expect(isReadonly(x.value)).toBe(false)
expect(isReadonly(x.value.a)).toBe(false)
const z = computed<typeof a>({
get() {
return a
},
set(v) {
a = v
},
})
expect(isReadonly(z)).toBe(false)
expect(isReadonly(z.value.a)).toBe(false)
})
it('should expose value when stopped', () => {
const x = computed(() => 1)
x.effect.stop()
expect(x.value).toBe(1)
})
2021-07-07 22:10:56 +08:00
it('debug: onTrack', () => {
let events: DebuggerEvent[] = []
const onTrack = vi.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = reactive({ foo: 1, bar: 2 })
const c = computed(() => (obj.foo, 'bar' in obj, Object.keys(obj)), {
onTrack,
})
expect(c.value).toEqual(['foo', 'bar'])
expect(onTrack).toHaveBeenCalledTimes(3)
expect(events).toEqual([
{
effect: c.effect,
target: toRaw(obj),
type: TrackOpTypes.GET,
key: 'foo',
},
{
effect: c.effect,
target: toRaw(obj),
type: TrackOpTypes.HAS,
key: 'bar',
},
{
effect: c.effect,
target: toRaw(obj),
type: TrackOpTypes.ITERATE,
key: ITERATE_KEY,
},
])
})
it('debug: onTrigger', () => {
let events: DebuggerEvent[] = []
const onTrigger = vi.fn((e: DebuggerEvent) => {
events.push(e)
})
const obj = reactive<{ foo?: number }>({ foo: 1 })
const c = computed(() => obj.foo, { onTrigger })
// computed won't trigger compute until accessed
c.value
obj.foo!++
expect(c.value).toBe(2)
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(events[0]).toEqual({
effect: c.effect,
target: toRaw(obj),
type: TriggerOpTypes.SET,
key: 'foo',
oldValue: 1,
newValue: 2,
})
delete obj.foo
expect(c.value).toBeUndefined()
expect(onTrigger).toHaveBeenCalledTimes(2)
expect(events[1]).toEqual({
effect: c.effect,
target: toRaw(obj),
type: TriggerOpTypes.DELETE,
key: 'foo',
oldValue: 2,
})
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
it('should query deps dirty sequentially', () => {
const cSpy = vi.fn()
const a = ref<null | { v: number }>({
v: 1,
})
const b = computed(() => {
return a.value
})
const c = computed(() => {
cSpy()
return b.value?.v
})
const d = computed(() => {
if (b.value) {
return c.value
}
return 0
})
d.value
a.value!.v = 2
a.value = null
d.value
expect(cSpy).toHaveBeenCalledTimes(1)
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
it('chained computed dirty reallocation after querying dirty', () => {
let _msg: string | undefined
const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})
effect(() => {
_msg = msg.value
})
items.value = [1, 2, 3]
items.value = [1, 2, 3]
items.value = undefined
expect(_msg).toBe('The items are not loaded')
})
it('chained computed dirty reallocation after trigger computed getter', () => {
let _msg: string | undefined
const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})
_msg = msg.value
items.value = [1, 2, 3]
isLoaded.value // <- trigger computed getter
_msg = msg.value
items.value = undefined
_msg = msg.value
expect(_msg).toBe('The items are not loaded')
})
// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
it('deps order should be consistent with the last time get value', () => {
const cSpy = vi.fn()
const a = ref(0)
const b = computed(() => {
return a.value % 3 !== 0
})
const c = computed(() => {
cSpy()
if (a.value % 3 === 2) {
return 'expensive'
}
return 'cheap'
})
const d = computed(() => {
return a.value % 3 === 2
})
const e = computed(() => {
if (b.value) {
if (d.value) {
return 'Avoiding expensive calculation'
}
}
return c.value
})
e.value
a.value++
e.value
expect(e.effect.deps.length).toBe(3)
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
e.value
expect(cSpy).toHaveBeenCalledTimes(2)
})
it('should trigger by the second computed that maybe dirty', () => {
const cSpy = vi.fn()
const src1 = ref(0)
const src2 = ref(0)
const c1 = computed(() => src1.value)
const c2 = computed(() => (src1.value % 2) + src2.value)
const c3 = computed(() => {
cSpy()
c1.value
c2.value
})
c3.value
src1.value = 2
c3.value
expect(cSpy).toHaveBeenCalledTimes(2)
src2.value = 1
c3.value
expect(cSpy).toHaveBeenCalledTimes(3)
})
it('should trigger the second effect', () => {
const fnSpy = vi.fn()
const v = ref(1)
const c = computed(() => v.value)
effect(() => {
c.value
})
effect(() => {
c.value
fnSpy()
})
expect(fnSpy).toBeCalledTimes(1)
v.value = 2
expect(fnSpy).toBeCalledTimes(2)
})
2018-09-21 06:03:59 +08:00
})