computed.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test'
  2. import {
  3. type DebuggerEvent,
  4. ITERATE_KEY,
  5. TrackOpTypes,
  6. TriggerOpTypes,
  7. type WritableComputedRef,
  8. computed,
  9. effect,
  10. isReadonly,
  11. reactive,
  12. ref,
  13. toRaw,
  14. } from '../src'
  15. import { DirtyLevels } from '../src/constants'
  16. describe('reactivity/computed', () => {
  17. it('should return updated value', () => {
  18. const value = reactive<{ foo?: number }>({})
  19. const cValue = computed(() => value.foo)
  20. expect(cValue.value).toBe(undefined)
  21. value.foo = 1
  22. expect(cValue.value).toBe(1)
  23. })
  24. it('should compute lazily', () => {
  25. const value = reactive<{ foo?: number }>({})
  26. const getter = vi.fn(() => value.foo)
  27. const cValue = computed(getter)
  28. // lazy
  29. expect(getter).not.toHaveBeenCalled()
  30. expect(cValue.value).toBe(undefined)
  31. expect(getter).toHaveBeenCalledTimes(1)
  32. // should not compute again
  33. cValue.value
  34. expect(getter).toHaveBeenCalledTimes(1)
  35. // should not compute until needed
  36. value.foo = 1
  37. expect(getter).toHaveBeenCalledTimes(1)
  38. // now it should compute
  39. expect(cValue.value).toBe(1)
  40. expect(getter).toHaveBeenCalledTimes(2)
  41. // should not compute again
  42. cValue.value
  43. expect(getter).toHaveBeenCalledTimes(2)
  44. })
  45. it('should trigger effect', () => {
  46. const value = reactive<{ foo?: number }>({})
  47. const cValue = computed(() => value.foo)
  48. let dummy
  49. effect(() => {
  50. dummy = cValue.value
  51. })
  52. expect(dummy).toBe(undefined)
  53. value.foo = 1
  54. expect(dummy).toBe(1)
  55. })
  56. it('should work when chained', () => {
  57. const value = reactive({ foo: 0 })
  58. const c1 = computed(() => value.foo)
  59. const c2 = computed(() => c1.value + 1)
  60. expect(c2.value).toBe(1)
  61. expect(c1.value).toBe(0)
  62. value.foo++
  63. expect(c2.value).toBe(2)
  64. expect(c1.value).toBe(1)
  65. })
  66. it('should trigger effect when chained', () => {
  67. const value = reactive({ foo: 0 })
  68. const getter1 = vi.fn(() => value.foo)
  69. const getter2 = vi.fn(() => {
  70. return c1.value + 1
  71. })
  72. const c1 = computed(getter1)
  73. const c2 = computed(getter2)
  74. let dummy
  75. effect(() => {
  76. dummy = c2.value
  77. })
  78. expect(dummy).toBe(1)
  79. expect(getter1).toHaveBeenCalledTimes(1)
  80. expect(getter2).toHaveBeenCalledTimes(1)
  81. value.foo++
  82. expect(dummy).toBe(2)
  83. // should not result in duplicate calls
  84. expect(getter1).toHaveBeenCalledTimes(2)
  85. expect(getter2).toHaveBeenCalledTimes(2)
  86. })
  87. it('should trigger effect when chained (mixed invocations)', () => {
  88. const value = reactive({ foo: 0 })
  89. const getter1 = vi.fn(() => value.foo)
  90. const getter2 = vi.fn(() => {
  91. return c1.value + 1
  92. })
  93. const c1 = computed(getter1)
  94. const c2 = computed(getter2)
  95. let dummy
  96. effect(() => {
  97. dummy = c1.value + c2.value
  98. })
  99. expect(dummy).toBe(1)
  100. expect(getter1).toHaveBeenCalledTimes(1)
  101. expect(getter2).toHaveBeenCalledTimes(1)
  102. value.foo++
  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 }>({})
  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.stop()
  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', () => {
  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. })
  161. // access plusOne, causing it to be non-dirty
  162. plusOne.value
  163. // mutate n
  164. n.value++
  165. // on the 2nd run, plusOne.value should have already updated.
  166. expect(plusOneValues).toMatchObject([1, 2])
  167. })
  168. it('should warn if trying to set a readonly computed', () => {
  169. const n = ref(1)
  170. const plusOne = computed(() => n.value + 1)
  171. ;(plusOne as WritableComputedRef<number>).value++ // Type cast to prevent TS from preventing the error
  172. expect(
  173. 'Write operation failed: computed value is readonly',
  174. ).toHaveBeenWarnedLast()
  175. })
  176. it('should be readonly', () => {
  177. let a = { a: 1 }
  178. const x = computed(() => a)
  179. expect(isReadonly(x)).toBe(true)
  180. expect(isReadonly(x.value)).toBe(false)
  181. expect(isReadonly(x.value.a)).toBe(false)
  182. const z = computed<typeof a>({
  183. get() {
  184. return a
  185. },
  186. set(v) {
  187. a = v
  188. },
  189. })
  190. expect(isReadonly(z)).toBe(false)
  191. expect(isReadonly(z.value.a)).toBe(false)
  192. })
  193. it('should expose value when stopped', () => {
  194. const x = computed(() => 1)
  195. x.effect.stop()
  196. expect(x.value).toBe(1)
  197. })
  198. it('debug: onTrack', () => {
  199. let events: DebuggerEvent[] = []
  200. const onTrack = vi.fn((e: DebuggerEvent) => {
  201. events.push(e)
  202. })
  203. const obj = reactive({ foo: 1, bar: 2 })
  204. const c = computed(() => (obj.foo, 'bar' in obj, Object.keys(obj)), {
  205. onTrack,
  206. })
  207. expect(c.value).toEqual(['foo', 'bar'])
  208. expect(onTrack).toHaveBeenCalledTimes(3)
  209. expect(events).toEqual([
  210. {
  211. effect: c.effect,
  212. target: toRaw(obj),
  213. type: TrackOpTypes.GET,
  214. key: 'foo',
  215. },
  216. {
  217. effect: c.effect,
  218. target: toRaw(obj),
  219. type: TrackOpTypes.HAS,
  220. key: 'bar',
  221. },
  222. {
  223. effect: c.effect,
  224. target: toRaw(obj),
  225. type: TrackOpTypes.ITERATE,
  226. key: ITERATE_KEY,
  227. },
  228. ])
  229. })
  230. it('debug: onTrigger', () => {
  231. let events: DebuggerEvent[] = []
  232. const onTrigger = vi.fn((e: DebuggerEvent) => {
  233. events.push(e)
  234. })
  235. const obj = reactive<{ foo?: number }>({ foo: 1 })
  236. const c = computed(() => obj.foo, { onTrigger })
  237. // computed won't trigger compute until accessed
  238. c.value
  239. obj.foo!++
  240. expect(c.value).toBe(2)
  241. expect(onTrigger).toHaveBeenCalledTimes(1)
  242. expect(events[0]).toEqual({
  243. effect: c.effect,
  244. target: toRaw(obj),
  245. type: TriggerOpTypes.SET,
  246. key: 'foo',
  247. oldValue: 1,
  248. newValue: 2,
  249. })
  250. delete obj.foo
  251. expect(c.value).toBeUndefined()
  252. expect(onTrigger).toHaveBeenCalledTimes(2)
  253. expect(events[1]).toEqual({
  254. effect: c.effect,
  255. target: toRaw(obj),
  256. type: TriggerOpTypes.DELETE,
  257. key: 'foo',
  258. oldValue: 2,
  259. })
  260. })
  261. // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
  262. it('should query deps dirty sequentially', () => {
  263. const cSpy = vi.fn()
  264. const a = ref<null | { v: number }>({
  265. v: 1,
  266. })
  267. const b = computed(() => {
  268. return a.value
  269. })
  270. const c = computed(() => {
  271. cSpy()
  272. return b.value?.v
  273. })
  274. const d = computed(() => {
  275. if (b.value) {
  276. return c.value
  277. }
  278. return 0
  279. })
  280. d.value
  281. a.value!.v = 2
  282. a.value = null
  283. d.value
  284. expect(cSpy).toHaveBeenCalledTimes(1)
  285. })
  286. // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
  287. it('chained computed dirty reallocation after querying dirty', () => {
  288. let _msg: string | undefined
  289. const items = ref<number[]>()
  290. const isLoaded = computed(() => {
  291. return !!items.value
  292. })
  293. const msg = computed(() => {
  294. if (isLoaded.value) {
  295. return 'The items are loaded'
  296. } else {
  297. return 'The items are not loaded'
  298. }
  299. })
  300. effect(() => {
  301. _msg = msg.value
  302. })
  303. items.value = [1, 2, 3]
  304. items.value = [1, 2, 3]
  305. items.value = undefined
  306. expect(_msg).toBe('The items are not loaded')
  307. })
  308. it('chained computed dirty reallocation after trigger computed getter', () => {
  309. let _msg: string | undefined
  310. const items = ref<number[]>()
  311. const isLoaded = computed(() => {
  312. return !!items.value
  313. })
  314. const msg = computed(() => {
  315. if (isLoaded.value) {
  316. return 'The items are loaded'
  317. } else {
  318. return 'The items are not loaded'
  319. }
  320. })
  321. _msg = msg.value
  322. items.value = [1, 2, 3]
  323. isLoaded.value // <- trigger computed getter
  324. _msg = msg.value
  325. items.value = undefined
  326. _msg = msg.value
  327. expect(_msg).toBe('The items are not loaded')
  328. })
  329. // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
  330. it('deps order should be consistent with the last time get value', () => {
  331. const cSpy = vi.fn()
  332. const a = ref(0)
  333. const b = computed(() => {
  334. return a.value % 3 !== 0
  335. })
  336. const c = computed(() => {
  337. cSpy()
  338. if (a.value % 3 === 2) {
  339. return 'expensive'
  340. }
  341. return 'cheap'
  342. })
  343. const d = computed(() => {
  344. return a.value % 3 === 2
  345. })
  346. const e = computed(() => {
  347. if (b.value) {
  348. if (d.value) {
  349. return 'Avoiding expensive calculation'
  350. }
  351. }
  352. return c.value
  353. })
  354. e.value
  355. a.value++
  356. e.value
  357. expect(e.effect.deps.length).toBe(3)
  358. expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
  359. expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
  360. expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
  361. expect(cSpy).toHaveBeenCalledTimes(2)
  362. a.value++
  363. e.value
  364. expect(cSpy).toHaveBeenCalledTimes(2)
  365. })
  366. it('should trigger by the second computed that maybe dirty', () => {
  367. const cSpy = vi.fn()
  368. const src1 = ref(0)
  369. const src2 = ref(0)
  370. const c1 = computed(() => src1.value)
  371. const c2 = computed(() => (src1.value % 2) + src2.value)
  372. const c3 = computed(() => {
  373. cSpy()
  374. c1.value
  375. c2.value
  376. })
  377. c3.value
  378. src1.value = 2
  379. c3.value
  380. expect(cSpy).toHaveBeenCalledTimes(2)
  381. src2.value = 1
  382. c3.value
  383. expect(cSpy).toHaveBeenCalledTimes(3)
  384. })
  385. it('should trigger the second effect', () => {
  386. const fnSpy = vi.fn()
  387. const v = ref(1)
  388. const c = computed(() => v.value)
  389. effect(() => {
  390. c.value
  391. })
  392. effect(() => {
  393. c.value
  394. fnSpy()
  395. })
  396. expect(fnSpy).toBeCalledTimes(1)
  397. v.value = 2
  398. expect(fnSpy).toBeCalledTimes(2)
  399. })
  400. it('should chained recurse effects clear dirty after trigger', () => {
  401. const v = ref(1)
  402. const c1 = computed(() => v.value)
  403. const c2 = computed(() => c1.value)
  404. c1.effect.allowRecurse = true
  405. c2.effect.allowRecurse = true
  406. c2.value
  407. expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
  408. expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
  409. })
  410. it('should chained computeds dirtyLevel update with first computed effect', () => {
  411. const v = ref(0)
  412. const c1 = computed(() => {
  413. if (v.value === 0) {
  414. v.value = 1
  415. }
  416. return v.value
  417. })
  418. const c2 = computed(() => c1.value)
  419. const c3 = computed(() => c2.value)
  420. c3.value
  421. expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
  422. expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
  423. expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
  424. })
  425. it('should work when chained(ref+computed)', () => {
  426. const v = ref(0)
  427. const c1 = computed(() => {
  428. if (v.value === 0) {
  429. v.value = 1
  430. }
  431. return 'foo'
  432. })
  433. const c2 = computed(() => v.value + c1.value)
  434. expect(c2.value).toBe('0foo')
  435. expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
  436. expect(c2.value).toBe('1foo')
  437. })
  438. it('should trigger effect even computed already dirty', () => {
  439. const fnSpy = vi.fn()
  440. const v = ref(0)
  441. const c1 = computed(() => {
  442. if (v.value === 0) {
  443. v.value = 1
  444. }
  445. return 'foo'
  446. })
  447. const c2 = computed(() => v.value + c1.value)
  448. effect(() => {
  449. fnSpy()
  450. c2.value
  451. })
  452. expect(fnSpy).toBeCalledTimes(1)
  453. expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
  454. expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
  455. v.value = 2
  456. expect(fnSpy).toBeCalledTimes(2)
  457. })
  458. it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
  459. const state = reactive<any>({})
  460. const consumer = computed(() => {
  461. if (!('a' in state)) state.a = 1
  462. return state.a
  463. })
  464. const Comp = {
  465. setup: () => {
  466. nextTick().then(() => {
  467. state.a = 2
  468. })
  469. return () => consumer.value
  470. },
  471. }
  472. const root = nodeOps.createElement('div')
  473. render(h(Comp), root)
  474. await nextTick()
  475. await nextTick()
  476. expect(serializeInner(root)).toBe(`2`)
  477. })
  478. })