computed.spec.ts 16 KB

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