computed.spec.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import {
  2. computed,
  3. reactive,
  4. ref,
  5. isReadonly,
  6. WritableComputedRef,
  7. DebuggerEvent,
  8. TrackOpTypes,
  9. TriggerOpTypes
  10. } from 'v3'
  11. import { effect } from 'v3/reactivity/effect'
  12. import { nextTick } from 'core/util'
  13. import { set, del } from 'core/observer/index'
  14. describe('reactivity/computed', () => {
  15. it('should return updated value', () => {
  16. const value = reactive({ foo: 1 })
  17. const cValue = computed(() => value.foo)
  18. expect(cValue.value).toBe(1)
  19. value.foo = 2
  20. expect(cValue.value).toBe(2)
  21. })
  22. it('should compute lazily', () => {
  23. const value = reactive<{ foo?: number }>({ foo: undefined })
  24. const getter = vi.fn(() => value.foo)
  25. const cValue = computed(getter)
  26. // lazy
  27. expect(getter).not.toHaveBeenCalled()
  28. expect(cValue.value).toBe(undefined)
  29. expect(getter).toHaveBeenCalledTimes(1)
  30. // should not compute again
  31. cValue.value
  32. expect(getter).toHaveBeenCalledTimes(1)
  33. // should not compute until needed
  34. value.foo = 1
  35. expect(getter).toHaveBeenCalledTimes(1)
  36. // now it should compute
  37. expect(cValue.value).toBe(1)
  38. expect(getter).toHaveBeenCalledTimes(2)
  39. // should not compute again
  40. cValue.value
  41. expect(getter).toHaveBeenCalledTimes(2)
  42. })
  43. it('should trigger effect', () => {
  44. const value = reactive<{ foo?: number }>({ foo: undefined })
  45. const cValue = computed(() => value.foo)
  46. let dummy
  47. effect(() => {
  48. dummy = cValue.value
  49. })
  50. expect(dummy).toBe(undefined)
  51. value.foo = 1
  52. expect(dummy).toBe(1)
  53. })
  54. it('should work when chained', () => {
  55. const value = reactive({ foo: 0 })
  56. const c1 = computed(() => value.foo)
  57. const c2 = computed(() => c1.value + 1)
  58. expect(c2.value).toBe(1)
  59. expect(c1.value).toBe(0)
  60. value.foo++
  61. expect(c2.value).toBe(2)
  62. expect(c1.value).toBe(1)
  63. })
  64. it('should trigger effect when chained', () => {
  65. const value = reactive({ foo: 0 })
  66. const getter1 = vi.fn(() => value.foo)
  67. const getter2 = vi.fn(() => {
  68. return c1.value + 1
  69. })
  70. const c1 = computed(getter1)
  71. const c2 = computed(getter2)
  72. let dummy
  73. effect(() => {
  74. dummy = c2.value
  75. })
  76. expect(dummy).toBe(1)
  77. expect(getter1).toHaveBeenCalledTimes(1)
  78. expect(getter2).toHaveBeenCalledTimes(1)
  79. value.foo++
  80. expect(dummy).toBe(2)
  81. // should not result in duplicate calls
  82. expect(getter1).toHaveBeenCalledTimes(2)
  83. expect(getter2).toHaveBeenCalledTimes(2)
  84. })
  85. it('should trigger effect when chained (mixed invocations)', async () => {
  86. const value = reactive({ foo: 0 })
  87. const getter1 = vi.fn(() => value.foo)
  88. const getter2 = vi.fn(() => {
  89. return c1.value + 1
  90. })
  91. const c1 = computed(getter1)
  92. const c2 = computed(getter2)
  93. let dummy
  94. // @discrepancy Vue 2 chained computed doesn't work with sync watchers
  95. effect(() => {
  96. dummy = c1.value + c2.value
  97. }, nextTick)
  98. expect(dummy).toBe(1)
  99. expect(getter1).toHaveBeenCalledTimes(1)
  100. expect(getter2).toHaveBeenCalledTimes(1)
  101. value.foo++
  102. await nextTick()
  103. expect(dummy).toBe(3)
  104. // should not result in duplicate calls
  105. expect(getter1).toHaveBeenCalledTimes(2)
  106. expect(getter2).toHaveBeenCalledTimes(2)
  107. })
  108. it('should no longer update when stopped', () => {
  109. const value = reactive<{ foo?: number }>({ foo: undefined })
  110. const cValue = computed(() => value.foo)
  111. let dummy
  112. effect(() => {
  113. dummy = cValue.value
  114. })
  115. expect(dummy).toBe(undefined)
  116. value.foo = 1
  117. expect(dummy).toBe(1)
  118. cValue.effect.teardown()
  119. value.foo = 2
  120. expect(dummy).toBe(1)
  121. })
  122. it('should support setter', () => {
  123. const n = ref(1)
  124. const plusOne = computed({
  125. get: () => n.value + 1,
  126. set: val => {
  127. n.value = val - 1
  128. }
  129. })
  130. expect(plusOne.value).toBe(2)
  131. n.value++
  132. expect(plusOne.value).toBe(3)
  133. plusOne.value = 0
  134. expect(n.value).toBe(-1)
  135. })
  136. it('should trigger effect w/ setter', () => {
  137. const n = ref(1)
  138. const plusOne = computed({
  139. get: () => n.value + 1,
  140. set: val => {
  141. n.value = val - 1
  142. }
  143. })
  144. let dummy
  145. effect(() => {
  146. dummy = n.value
  147. })
  148. expect(dummy).toBe(1)
  149. plusOne.value = 0
  150. expect(dummy).toBe(-1)
  151. })
  152. // #5720
  153. it('should invalidate before non-computed effects', async () => {
  154. let plusOneValues: number[] = []
  155. const n = ref(0)
  156. const plusOne = computed(() => n.value + 1)
  157. effect(() => {
  158. n.value
  159. plusOneValues.push(plusOne.value)
  160. }, nextTick)
  161. expect(plusOneValues).toMatchObject([1])
  162. // access plusOne, causing it to be non-dirty
  163. plusOne.value
  164. // mutate n
  165. n.value++
  166. await nextTick()
  167. // on the 2nd run, plusOne.value should have already updated.
  168. expect(plusOneValues).toMatchObject([1, 2])
  169. })
  170. it('should warn if trying to set a readonly computed', () => {
  171. const n = ref(1)
  172. const plusOne = computed(() => n.value + 1)
  173. ;(plusOne as WritableComputedRef<number>).value++ // Type cast to prevent TS from preventing the error
  174. expect(
  175. 'Write operation failed: computed value is readonly'
  176. ).toHaveBeenWarnedLast()
  177. })
  178. it('should be readonly', () => {
  179. let a = { a: 1 }
  180. const x = computed(() => a)
  181. expect(isReadonly(x)).toBe(true)
  182. expect(isReadonly(x.value)).toBe(false)
  183. expect(isReadonly(x.value.a)).toBe(false)
  184. const z = computed<typeof a>({
  185. get() {
  186. return a
  187. },
  188. set(v) {
  189. a = v
  190. }
  191. })
  192. expect(isReadonly(z)).toBe(false)
  193. expect(isReadonly(z.value.a)).toBe(false)
  194. })
  195. it('should expose value when stopped', () => {
  196. const x = computed(() => 1)
  197. x.effect.teardown()
  198. expect(x.value).toBe(1)
  199. })
  200. it('debug: onTrack', () => {
  201. let events: DebuggerEvent[] = []
  202. const onTrack = vi.fn((e: DebuggerEvent) => {
  203. events.push(e)
  204. })
  205. const obj = reactive({ foo: 1, bar: 2 })
  206. const c = computed(() => obj.foo + obj.bar, {
  207. onTrack
  208. })
  209. expect(c.value).toEqual(3)
  210. expect(onTrack).toHaveBeenCalledTimes(2)
  211. expect(events).toEqual([
  212. {
  213. effect: c.effect,
  214. target: obj,
  215. type: TrackOpTypes.GET,
  216. key: 'foo'
  217. },
  218. {
  219. effect: c.effect,
  220. target: obj,
  221. type: TrackOpTypes.GET,
  222. key: 'bar'
  223. }
  224. ])
  225. })
  226. it('debug: onTrigger', () => {
  227. let events: DebuggerEvent[] = []
  228. const onTrigger = vi.fn((e: DebuggerEvent) => {
  229. events.push(e)
  230. })
  231. const obj = reactive({ foo: 1, bar: { baz: 2 } })
  232. const c = computed(() => obj.foo + (obj.bar.baz || 0), { onTrigger })
  233. // computed won't trigger compute until accessed
  234. c.value
  235. obj.foo++
  236. expect(c.value).toBe(4)
  237. expect(onTrigger).toHaveBeenCalledTimes(1)
  238. expect(events[0]).toEqual({
  239. effect: c.effect,
  240. target: obj,
  241. type: TriggerOpTypes.SET,
  242. key: 'foo',
  243. oldValue: 1,
  244. newValue: 2
  245. })
  246. del(obj.bar, 'baz')
  247. expect(c.value).toBe(2)
  248. expect(onTrigger).toHaveBeenCalledTimes(2)
  249. expect(events[1]).toEqual({
  250. effect: c.effect,
  251. target: obj.bar,
  252. type: TriggerOpTypes.DELETE,
  253. key: 'baz'
  254. })
  255. set(obj.bar, 'baz', 1)
  256. expect(c.value).toBe(3)
  257. expect(onTrigger).toHaveBeenCalledTimes(3)
  258. expect(events[2]).toEqual({
  259. effect: c.effect,
  260. target: obj.bar,
  261. type: TriggerOpTypes.ADD,
  262. key: 'baz',
  263. oldValue: undefined,
  264. newValue: 1
  265. })
  266. })
  267. })