computed.spec.ts 15 KB

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