computed.spec.ts 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  1. import {
  2. type TestElement,
  3. defineComponent,
  4. h,
  5. nextTick,
  6. nodeOps,
  7. onMounted,
  8. onUnmounted,
  9. render,
  10. serializeInner,
  11. triggerEvent,
  12. } from '@vue/runtime-test'
  13. import {
  14. type DebuggerEvent,
  15. ITERATE_KEY,
  16. TrackOpTypes,
  17. TriggerOpTypes,
  18. type WritableComputedRef,
  19. computed,
  20. effect,
  21. isReadonly,
  22. reactive,
  23. ref,
  24. shallowRef,
  25. toRaw,
  26. triggerRef,
  27. } from '../src'
  28. import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
  29. import type { ComputedRef, ComputedRefImpl } from '../src/computed'
  30. describe('reactivity/computed', () => {
  31. it('should return updated value', () => {
  32. const value = reactive<{ foo?: number }>({})
  33. const cValue = computed(() => value.foo)
  34. expect(cValue.value).toBe(undefined)
  35. value.foo = 1
  36. expect(cValue.value).toBe(1)
  37. })
  38. it('pass oldValue to computed getter', () => {
  39. const count = ref(0)
  40. const oldValue = ref()
  41. const curValue = computed(pre => {
  42. oldValue.value = pre
  43. return count.value
  44. })
  45. expect(curValue.value).toBe(0)
  46. expect(oldValue.value).toBe(undefined)
  47. count.value++
  48. expect(curValue.value).toBe(1)
  49. expect(oldValue.value).toBe(0)
  50. })
  51. it('should compute lazily', () => {
  52. const value = reactive<{ foo?: number }>({})
  53. const getter = vi.fn(() => value.foo)
  54. const cValue = computed(getter)
  55. // lazy
  56. expect(getter).not.toHaveBeenCalled()
  57. expect(cValue.value).toBe(undefined)
  58. expect(getter).toHaveBeenCalledTimes(1)
  59. // should not compute again
  60. cValue.value
  61. expect(getter).toHaveBeenCalledTimes(1)
  62. // should not compute until needed
  63. value.foo = 1
  64. expect(getter).toHaveBeenCalledTimes(1)
  65. // now it should compute
  66. expect(cValue.value).toBe(1)
  67. expect(getter).toHaveBeenCalledTimes(2)
  68. // should not compute again
  69. cValue.value
  70. expect(getter).toHaveBeenCalledTimes(2)
  71. })
  72. it('should trigger effect', () => {
  73. const value = reactive<{ foo?: number }>({})
  74. const cValue = computed(() => value.foo)
  75. let dummy
  76. effect(() => {
  77. dummy = cValue.value
  78. })
  79. expect(dummy).toBe(undefined)
  80. value.foo = 1
  81. expect(dummy).toBe(1)
  82. })
  83. it('should work when chained', () => {
  84. const value = reactive({ foo: 0 })
  85. const c1 = computed(() => value.foo)
  86. const c2 = computed(() => c1.value + 1)
  87. expect(c2.value).toBe(1)
  88. expect(c1.value).toBe(0)
  89. value.foo++
  90. expect(c2.value).toBe(2)
  91. expect(c1.value).toBe(1)
  92. })
  93. it('should trigger effect when chained', () => {
  94. const value = reactive({ foo: 0 })
  95. const getter1 = vi.fn(() => value.foo)
  96. const getter2 = vi.fn(() => {
  97. return c1.value + 1
  98. })
  99. const c1 = computed(getter1)
  100. const c2 = computed(getter2)
  101. let dummy
  102. effect(() => {
  103. dummy = c2.value
  104. })
  105. expect(dummy).toBe(1)
  106. expect(getter1).toHaveBeenCalledTimes(1)
  107. expect(getter2).toHaveBeenCalledTimes(1)
  108. value.foo++
  109. expect(dummy).toBe(2)
  110. // should not result in duplicate calls
  111. expect(getter1).toHaveBeenCalledTimes(2)
  112. expect(getter2).toHaveBeenCalledTimes(2)
  113. })
  114. it('should trigger effect when chained (mixed invocations)', () => {
  115. const value = reactive({ foo: 0 })
  116. const getter1 = vi.fn(() => value.foo)
  117. const getter2 = vi.fn(() => {
  118. return c1.value + 1
  119. })
  120. const c1 = computed(getter1)
  121. const c2 = computed(getter2)
  122. let dummy
  123. effect(() => {
  124. dummy = c1.value + c2.value
  125. })
  126. expect(dummy).toBe(1)
  127. expect(getter1).toHaveBeenCalledTimes(1)
  128. expect(getter2).toHaveBeenCalledTimes(1)
  129. value.foo++
  130. expect(dummy).toBe(3)
  131. // should not result in duplicate calls
  132. expect(getter1).toHaveBeenCalledTimes(2)
  133. expect(getter2).toHaveBeenCalledTimes(2)
  134. })
  135. it('should support setter', () => {
  136. const n = ref(1)
  137. const plusOne = computed({
  138. get: () => n.value + 1,
  139. set: val => {
  140. n.value = val - 1
  141. },
  142. })
  143. expect(plusOne.value).toBe(2)
  144. n.value++
  145. expect(plusOne.value).toBe(3)
  146. plusOne.value = 0
  147. expect(n.value).toBe(-1)
  148. })
  149. it('should trigger effect w/ setter', () => {
  150. const n = ref(1)
  151. const plusOne = computed({
  152. get: () => n.value + 1,
  153. set: val => {
  154. n.value = val - 1
  155. },
  156. })
  157. let dummy
  158. effect(() => {
  159. dummy = n.value
  160. })
  161. expect(dummy).toBe(1)
  162. plusOne.value = 0
  163. expect(dummy).toBe(-1)
  164. })
  165. // #5720
  166. it('should invalidate before non-computed effects', () => {
  167. let plusOneValues: number[] = []
  168. const n = ref(0)
  169. const plusOne = computed(() => n.value + 1)
  170. effect(() => {
  171. n.value
  172. plusOneValues.push(plusOne.value)
  173. })
  174. // access plusOne, causing it to be non-dirty
  175. plusOne.value
  176. // mutate n
  177. n.value++
  178. // on the 2nd run, plusOne.value should have already updated.
  179. expect(plusOneValues).toMatchObject([1, 2])
  180. })
  181. it('should warn if trying to set a readonly computed', () => {
  182. const n = ref(1)
  183. const plusOne = computed(() => n.value + 1)
  184. ;(plusOne as WritableComputedRef<number>).value++ // Type cast to prevent TS from preventing the error
  185. expect(
  186. 'Write operation failed: computed value is readonly',
  187. ).toHaveBeenWarnedLast()
  188. })
  189. it('should be readonly', () => {
  190. let a = { a: 1 }
  191. const x = computed(() => a)
  192. expect(isReadonly(x)).toBe(true)
  193. expect(isReadonly(x.value)).toBe(false)
  194. expect(isReadonly(x.value.a)).toBe(false)
  195. const z = computed<typeof a>({
  196. get() {
  197. return a
  198. },
  199. set(v) {
  200. a = v
  201. },
  202. })
  203. expect(isReadonly(z)).toBe(false)
  204. expect(isReadonly(z.value.a)).toBe(false)
  205. })
  206. it('debug: onTrack', () => {
  207. let events: DebuggerEvent[] = []
  208. const onTrack = vi.fn((e: DebuggerEvent) => {
  209. events.push(e)
  210. })
  211. const obj = reactive({ foo: 1, bar: 2 })
  212. const c = computed(() => (obj.foo, 'bar' in obj, Object.keys(obj)), {
  213. onTrack,
  214. })
  215. expect(c.value).toEqual(['foo', 'bar'])
  216. expect(onTrack).toHaveBeenCalledTimes(3)
  217. expect(events).toEqual([
  218. {
  219. effect: c,
  220. target: toRaw(obj),
  221. type: TrackOpTypes.GET,
  222. key: 'foo',
  223. },
  224. {
  225. effect: c,
  226. target: toRaw(obj),
  227. type: TrackOpTypes.HAS,
  228. key: 'bar',
  229. },
  230. {
  231. effect: c,
  232. target: toRaw(obj),
  233. type: TrackOpTypes.ITERATE,
  234. key: ITERATE_KEY,
  235. },
  236. ])
  237. })
  238. it('debug: onTrigger (reactive)', () => {
  239. let events: DebuggerEvent[] = []
  240. const onTrigger = vi.fn((e: DebuggerEvent) => {
  241. events.push(e)
  242. })
  243. const obj = reactive<{ foo?: number }>({ foo: 1 })
  244. const c = computed(() => obj.foo, { onTrigger })
  245. // computed won't track until it has a subscriber
  246. effect(() => c.value)
  247. obj.foo!++
  248. expect(c.value).toBe(2)
  249. expect(onTrigger).toHaveBeenCalledTimes(1)
  250. expect(events[0]).toEqual({
  251. effect: c,
  252. target: toRaw(obj),
  253. type: TriggerOpTypes.SET,
  254. key: 'foo',
  255. oldValue: 1,
  256. newValue: 2,
  257. })
  258. delete obj.foo
  259. expect(c.value).toBeUndefined()
  260. expect(onTrigger).toHaveBeenCalledTimes(2)
  261. expect(events[1]).toEqual({
  262. effect: c,
  263. target: toRaw(obj),
  264. type: TriggerOpTypes.DELETE,
  265. key: 'foo',
  266. oldValue: 2,
  267. })
  268. })
  269. // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
  270. it('should query deps dirty sequentially', () => {
  271. const cSpy = vi.fn()
  272. const a = ref<null | { v: number }>({
  273. v: 1,
  274. })
  275. const b = computed(() => {
  276. return a.value
  277. })
  278. const c = computed(() => {
  279. cSpy()
  280. return b.value?.v
  281. })
  282. const d = computed(() => {
  283. if (b.value) {
  284. return c.value
  285. }
  286. return 0
  287. })
  288. d.value
  289. a.value!.v = 2
  290. a.value = null
  291. d.value
  292. expect(cSpy).toHaveBeenCalledTimes(1)
  293. })
  294. // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
  295. it('chained computed dirty reallocation after querying dirty', () => {
  296. let _msg: string | undefined
  297. const items = ref<number[]>()
  298. const isLoaded = computed(() => {
  299. return !!items.value
  300. })
  301. const msg = computed(() => {
  302. if (isLoaded.value) {
  303. return 'The items are loaded'
  304. } else {
  305. return 'The items are not loaded'
  306. }
  307. })
  308. effect(() => {
  309. _msg = msg.value
  310. })
  311. items.value = [1, 2, 3]
  312. items.value = [1, 2, 3]
  313. items.value = undefined
  314. expect(_msg).toBe('The items are not loaded')
  315. })
  316. it('chained computed dirty reallocation after trigger computed getter', () => {
  317. let _msg: string | undefined
  318. const items = ref<number[]>()
  319. const isLoaded = computed(() => {
  320. return !!items.value
  321. })
  322. const msg = computed(() => {
  323. if (isLoaded.value) {
  324. return 'The items are loaded'
  325. } else {
  326. return 'The items are not loaded'
  327. }
  328. })
  329. _msg = msg.value
  330. items.value = [1, 2, 3]
  331. isLoaded.value // <- trigger computed getter
  332. _msg = msg.value
  333. items.value = undefined
  334. _msg = msg.value
  335. expect(_msg).toBe('The items are not loaded')
  336. })
  337. // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
  338. it('deps order should be consistent with the last time get value', () => {
  339. const cSpy = vi.fn()
  340. const a = ref(0)
  341. const b = computed(() => {
  342. return a.value % 3 !== 0
  343. }) as unknown as ComputedRefImpl
  344. const c = computed(() => {
  345. cSpy()
  346. if (a.value % 3 === 2) {
  347. return 'expensive'
  348. }
  349. return 'cheap'
  350. }) as unknown as ComputedRefImpl
  351. const d = computed(() => {
  352. return a.value % 3 === 2
  353. }) as unknown as ComputedRefImpl
  354. const e = computed(() => {
  355. if (b.value) {
  356. if (d.value) {
  357. return 'Avoiding expensive calculation'
  358. }
  359. }
  360. return c.value
  361. }) as unknown as ComputedRefImpl
  362. e.value
  363. a.value++
  364. e.value
  365. expect(e.deps!.dep).toBe(b.dep)
  366. expect(e.deps!.nextDep!.dep).toBe(d.dep)
  367. expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
  368. expect(cSpy).toHaveBeenCalledTimes(2)
  369. a.value++
  370. e.value
  371. expect(cSpy).toHaveBeenCalledTimes(2)
  372. })
  373. it('should trigger by the second computed that maybe dirty', () => {
  374. const cSpy = vi.fn()
  375. const src1 = ref(0)
  376. const src2 = ref(0)
  377. const c1 = computed(() => src1.value)
  378. const c2 = computed(() => (src1.value % 2) + src2.value)
  379. const c3 = computed(() => {
  380. cSpy()
  381. c1.value
  382. c2.value
  383. })
  384. c3.value
  385. src1.value = 2
  386. c3.value
  387. expect(cSpy).toHaveBeenCalledTimes(2)
  388. src2.value = 1
  389. c3.value
  390. expect(cSpy).toHaveBeenCalledTimes(3)
  391. })
  392. it('should trigger the second effect', () => {
  393. const fnSpy = vi.fn()
  394. const v = ref(1)
  395. const c = computed(() => v.value)
  396. effect(() => {
  397. c.value
  398. })
  399. effect(() => {
  400. c.value
  401. fnSpy()
  402. })
  403. expect(fnSpy).toBeCalledTimes(1)
  404. v.value = 2
  405. expect(fnSpy).toBeCalledTimes(2)
  406. })
  407. it('should chained recursive effects clear dirty after trigger', () => {
  408. const v = ref(1)
  409. const c1 = computed(() => v.value) as unknown as ComputedRefImpl
  410. const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
  411. c2.value
  412. expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
  413. expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
  414. })
  415. it('should chained computeds dirtyLevel update with first computed effect', () => {
  416. const v = ref(0)
  417. const c1 = computed(() => {
  418. if (v.value === 0) {
  419. v.value = 1
  420. }
  421. return v.value
  422. })
  423. const c2 = computed(() => c1.value)
  424. const c3 = computed(() => c2.value)
  425. c3.value
  426. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  427. })
  428. it('should work when chained(ref+computed)', () => {
  429. const v = ref(0)
  430. const c1 = computed(() => {
  431. if (v.value === 0) {
  432. v.value = 1
  433. }
  434. return 'foo'
  435. })
  436. const c2 = computed(() => v.value + c1.value)
  437. expect(c2.value).toBe('0foo')
  438. expect(c2.value).toBe('1foo')
  439. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  440. })
  441. it('should trigger effect even computed already dirty', () => {
  442. const fnSpy = vi.fn()
  443. const v = ref(0)
  444. const c1 = computed(() => {
  445. if (v.value === 0) {
  446. v.value = 1
  447. }
  448. return 'foo'
  449. })
  450. const c2 = computed(() => v.value + c1.value)
  451. effect(() => {
  452. fnSpy(c2.value)
  453. })
  454. expect(fnSpy).toBeCalledTimes(1)
  455. expect(fnSpy.mock.calls).toMatchObject([['0foo']])
  456. expect(v.value).toBe(1)
  457. v.value = 2
  458. expect(fnSpy).toBeCalledTimes(2)
  459. expect(fnSpy.mock.calls).toMatchObject([['0foo'], ['2foo']])
  460. expect(v.value).toBe(2)
  461. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  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. c3.value
  486. v1.value.v.value = 999
  487. expect(c3.value).toBe('yes')
  488. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  489. })
  490. it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
  491. const state = reactive<any>({})
  492. const consumer = computed(() => {
  493. if (!('a' in state)) state.a = 1
  494. return state.a
  495. })
  496. const Comp = {
  497. setup: () => {
  498. nextTick().then(() => {
  499. state.a = 2
  500. })
  501. return () => consumer.value
  502. },
  503. }
  504. const root = nodeOps.createElement('div')
  505. render(h(Comp), root)
  506. await nextTick()
  507. await nextTick()
  508. expect(serializeInner(root)).toBe(`2`)
  509. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  510. })
  511. it('should not trigger effect scheduler by recursive computed effect', async () => {
  512. const v = ref('Hello')
  513. const c = computed(() => {
  514. v.value += ' World'
  515. return v.value
  516. })
  517. const Comp = {
  518. setup: () => {
  519. return () => c.value
  520. },
  521. }
  522. const root = nodeOps.createElement('div')
  523. render(h(Comp), root)
  524. await nextTick()
  525. expect(serializeInner(root)).toBe('Hello World')
  526. v.value += ' World'
  527. await nextTick()
  528. expect(serializeInner(root)).toBe('Hello World World World World')
  529. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  530. })
  531. test('should not trigger if value did not change', () => {
  532. const src = ref(0)
  533. const c = computed(() => src.value % 2)
  534. const spy = vi.fn()
  535. effect(() => {
  536. spy(c.value)
  537. })
  538. expect(spy).toHaveBeenCalledTimes(1)
  539. src.value = 2
  540. // should not trigger
  541. expect(spy).toHaveBeenCalledTimes(1)
  542. src.value = 3
  543. src.value = 5
  544. // should trigger because latest value changes
  545. expect(spy).toHaveBeenCalledTimes(2)
  546. })
  547. test('chained computed trigger', () => {
  548. const effectSpy = vi.fn()
  549. const c1Spy = vi.fn()
  550. const c2Spy = vi.fn()
  551. const src = ref(0)
  552. const c1 = computed(() => {
  553. c1Spy()
  554. return src.value % 2
  555. })
  556. const c2 = computed(() => {
  557. c2Spy()
  558. return c1.value + 1
  559. })
  560. effect(() => {
  561. effectSpy(c2.value)
  562. })
  563. expect(c1Spy).toHaveBeenCalledTimes(1)
  564. expect(c2Spy).toHaveBeenCalledTimes(1)
  565. expect(effectSpy).toHaveBeenCalledTimes(1)
  566. src.value = 1
  567. expect(c1Spy).toHaveBeenCalledTimes(2)
  568. expect(c2Spy).toHaveBeenCalledTimes(2)
  569. expect(effectSpy).toHaveBeenCalledTimes(2)
  570. })
  571. test('chained computed avoid re-compute', () => {
  572. const effectSpy = vi.fn()
  573. const c1Spy = vi.fn()
  574. const c2Spy = vi.fn()
  575. const src = ref(0)
  576. const c1 = computed(() => {
  577. c1Spy()
  578. return src.value % 2
  579. })
  580. const c2 = computed(() => {
  581. c2Spy()
  582. return c1.value + 1
  583. })
  584. effect(() => {
  585. effectSpy(c2.value)
  586. })
  587. expect(effectSpy).toHaveBeenCalledTimes(1)
  588. src.value = 2
  589. src.value = 4
  590. src.value = 6
  591. expect(c1Spy).toHaveBeenCalledTimes(4)
  592. // c2 should not have to re-compute because c1 did not change.
  593. expect(c2Spy).toHaveBeenCalledTimes(1)
  594. // effect should not trigger because c2 did not change.
  595. expect(effectSpy).toHaveBeenCalledTimes(1)
  596. })
  597. test('chained computed value invalidation', () => {
  598. const effectSpy = vi.fn()
  599. const c1Spy = vi.fn()
  600. const c2Spy = vi.fn()
  601. const src = ref(0)
  602. const c1 = computed(() => {
  603. c1Spy()
  604. return src.value % 2
  605. })
  606. const c2 = computed(() => {
  607. c2Spy()
  608. return c1.value + 1
  609. })
  610. effect(() => {
  611. effectSpy(c2.value)
  612. })
  613. expect(effectSpy).toHaveBeenCalledTimes(1)
  614. expect(effectSpy).toHaveBeenCalledWith(1)
  615. expect(c2.value).toBe(1)
  616. expect(c1Spy).toHaveBeenCalledTimes(1)
  617. expect(c2Spy).toHaveBeenCalledTimes(1)
  618. src.value = 1
  619. // value should be available sync
  620. expect(c2.value).toBe(2)
  621. expect(c2Spy).toHaveBeenCalledTimes(2)
  622. })
  623. test('sync access of invalidated chained computed should not prevent final effect from running', () => {
  624. const effectSpy = vi.fn()
  625. const c1Spy = vi.fn()
  626. const c2Spy = vi.fn()
  627. const src = ref(0)
  628. const c1 = computed(() => {
  629. c1Spy()
  630. return src.value % 2
  631. })
  632. const c2 = computed(() => {
  633. c2Spy()
  634. return c1.value + 1
  635. })
  636. effect(() => {
  637. effectSpy(c2.value)
  638. })
  639. expect(effectSpy).toHaveBeenCalledTimes(1)
  640. src.value = 1
  641. // sync access c2
  642. c2.value
  643. expect(effectSpy).toHaveBeenCalledTimes(2)
  644. })
  645. it('computed should force track in untracked zone', () => {
  646. const n = ref(0)
  647. const spy1 = vi.fn()
  648. const spy2 = vi.fn()
  649. let c: ComputedRef
  650. effect(() => {
  651. spy1()
  652. pauseTracking()
  653. n.value
  654. c = computed(() => n.value + 1)
  655. // access computed now to force refresh
  656. c.value
  657. effect(() => spy2(c.value))
  658. n.value
  659. resetTracking()
  660. })
  661. expect(spy1).toHaveBeenCalledTimes(1)
  662. expect(spy2).toHaveBeenCalledTimes(1)
  663. n.value++
  664. // outer effect should not trigger
  665. expect(spy1).toHaveBeenCalledTimes(1)
  666. // inner effect should trigger
  667. expect(spy2).toHaveBeenCalledTimes(2)
  668. })
  669. // not recommended behavior, but needed for backwards compatibility
  670. // used in VueUse asyncComputed
  671. it('computed side effect should be able trigger', () => {
  672. const a = ref(false)
  673. const b = ref(false)
  674. const c = computed(() => {
  675. a.value = true
  676. return b.value
  677. })
  678. effect(() => {
  679. if (a.value) {
  680. b.value = true
  681. }
  682. })
  683. expect(b.value).toBe(false)
  684. // accessing c triggers change
  685. c.value
  686. expect(b.value).toBe(true)
  687. expect(c.value).toBe(true)
  688. })
  689. it('chained computed should work when accessed before having subs', () => {
  690. const n = ref(0)
  691. const c = computed(() => n.value)
  692. const d = computed(() => c.value + 1)
  693. const spy = vi.fn()
  694. // access
  695. d.value
  696. let dummy
  697. effect(() => {
  698. spy()
  699. dummy = d.value
  700. })
  701. expect(spy).toHaveBeenCalledTimes(1)
  702. expect(dummy).toBe(1)
  703. n.value++
  704. expect(spy).toHaveBeenCalledTimes(2)
  705. expect(dummy).toBe(2)
  706. })
  707. // #10236
  708. it('chained computed should still refresh after owner component unmount', async () => {
  709. const a = ref(0)
  710. const spy = vi.fn()
  711. const Child = {
  712. setup() {
  713. const b = computed(() => a.value + 1)
  714. const c = computed(() => b.value + 1)
  715. // access
  716. c.value
  717. onUnmounted(() => spy(c.value))
  718. return () => {}
  719. },
  720. }
  721. const show = ref(true)
  722. const Parent = {
  723. setup() {
  724. return () => (show.value ? h(Child) : null)
  725. },
  726. }
  727. render(h(Parent), nodeOps.createElement('div'))
  728. a.value++
  729. show.value = false
  730. await nextTick()
  731. expect(spy).toHaveBeenCalledWith(3)
  732. })
  733. // case: radix-vue `useForwardExpose` sets a template ref during mount,
  734. // and checks for the element's closest form element in a computed.
  735. // the computed is expected to only evaluate after mount.
  736. it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => {
  737. const calls: string[] = []
  738. const a = ref(0)
  739. const b = computed(() => {
  740. calls.push('b eval')
  741. return a.value + 1
  742. })
  743. const App = {
  744. setup() {
  745. onMounted(() => {
  746. calls.push('mounted')
  747. })
  748. return () =>
  749. h(
  750. 'div',
  751. {
  752. ref: () => (a.value = 1),
  753. },
  754. b.value,
  755. )
  756. },
  757. }
  758. render(h(App), nodeOps.createElement('div'))
  759. await nextTick()
  760. expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
  761. })
  762. it('should chained computeds keep reactivity when computed effect happens', async () => {
  763. const v = ref('Hello')
  764. const c = computed(() => {
  765. v.value += ' World'
  766. return v.value
  767. })
  768. const d = computed(() => c.value)
  769. const e = computed(() => d.value)
  770. const Comp = {
  771. setup: () => {
  772. return () => d.value + ' | ' + e.value
  773. },
  774. }
  775. const root = nodeOps.createElement('div')
  776. render(h(Comp), root)
  777. await nextTick()
  778. expect(serializeInner(root)).toBe('Hello World | Hello World')
  779. v.value += ' World'
  780. await nextTick()
  781. expect(serializeInner(root)).toBe(
  782. 'Hello World World World World | Hello World World World World',
  783. )
  784. })
  785. it('should keep dirty level when side effect computed value changed', () => {
  786. const v = ref(0)
  787. const c = computed(() => {
  788. v.value += 1
  789. return v.value
  790. })
  791. const d = computed(() => {
  792. return { d: c.value }
  793. })
  794. const Comp = {
  795. setup: () => {
  796. return () => {
  797. return [d.value.d, d.value.d]
  798. }
  799. },
  800. }
  801. const root = nodeOps.createElement('div')
  802. render(h(Comp), root)
  803. expect(d.value.d).toBe(1)
  804. expect(serializeInner(root)).toBe('11')
  805. })
  806. it('should be recomputed without being affected by side effects', () => {
  807. const v = ref(0)
  808. const c1 = computed(() => {
  809. v.value = 1
  810. return 0
  811. })
  812. const c2 = computed(() => {
  813. return v.value + ',' + c1.value
  814. })
  815. expect(c2.value).toBe('0,0')
  816. v.value = 1
  817. expect(c2.value).toBe('1,0')
  818. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  819. })
  820. it('debug: onTrigger (ref)', () => {
  821. let events: DebuggerEvent[] = []
  822. const onTrigger = vi.fn((e: DebuggerEvent) => {
  823. events.push(e)
  824. })
  825. const obj = ref(1)
  826. const c = computed(() => obj.value, { onTrigger })
  827. // computed won't track until it has a subscriber
  828. effect(() => c.value)
  829. obj.value++
  830. expect(c.value).toBe(2)
  831. expect(onTrigger).toHaveBeenCalledTimes(1)
  832. expect(events[0]).toEqual({
  833. effect: c,
  834. target: toRaw(obj),
  835. type: TriggerOpTypes.SET,
  836. key: 'value',
  837. oldValue: 1,
  838. newValue: 2,
  839. })
  840. })
  841. // #11797
  842. test('should prevent endless recursion in self-referencing computed getters', async () => {
  843. const Comp = defineComponent({
  844. data() {
  845. return {
  846. counter: 0,
  847. }
  848. },
  849. computed: {
  850. message(): string {
  851. if (this.counter === 0) {
  852. this.counter++
  853. return this.message
  854. } else {
  855. return `Step ${this.counter}`
  856. }
  857. },
  858. },
  859. render() {
  860. return [
  861. h(
  862. 'button',
  863. {
  864. onClick: () => {
  865. this.counter++
  866. },
  867. },
  868. 'Step',
  869. ),
  870. h('p', this.message),
  871. ]
  872. },
  873. })
  874. const root = nodeOps.createElement('div')
  875. render(h(Comp), root)
  876. expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 1</p>`)
  877. triggerEvent(root.children[1] as TestElement, 'click')
  878. await nextTick()
  879. expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 2</p>`)
  880. })
  881. test('manual trigger computed', () => {
  882. const cValue = computed(() => 1)
  883. triggerRef(cValue)
  884. expect(cValue.value).toBe(1)
  885. })
  886. test('should not recompute if computed does not track reactive data', async () => {
  887. const spy = vi.fn()
  888. const c1 = computed(() => spy())
  889. c1.value
  890. ref(0).value++ // update globalVersion
  891. c1.value
  892. expect(spy).toBeCalledTimes(1)
  893. })
  894. test('computed should remain live after losing all subscribers', () => {
  895. const state = reactive({ a: 1 })
  896. const p = computed(() => state.a + 1)
  897. const { effect: e } = effect(() => p.value)
  898. e.stop()
  899. expect(p.value).toBe(2)
  900. state.a++
  901. expect(p.value).toBe(3)
  902. })
  903. // #11995
  904. test('computed dep cleanup should not cause property dep to be deleted', () => {
  905. const toggle = ref(true)
  906. const state = reactive({ a: 1 })
  907. const p = computed(() => {
  908. return toggle.value ? state.a : 111
  909. })
  910. const pp = computed(() => state.a)
  911. effect(() => p.value)
  912. expect(pp.value).toBe(1)
  913. toggle.value = false
  914. state.a++
  915. expect(pp.value).toBe(2)
  916. })
  917. // #12020
  918. test('computed value updates correctly after dep cleanup', () => {
  919. const obj = reactive({ foo: 1, flag: 1 })
  920. const c1 = computed(() => obj.foo)
  921. let foo
  922. effect(() => {
  923. foo = obj.flag ? (obj.foo, c1.value) : 0
  924. })
  925. expect(foo).toBe(1)
  926. obj.flag = 0
  927. expect(foo).toBe(0)
  928. obj.foo = 2
  929. obj.flag = 1
  930. expect(foo).toBe(2)
  931. })
  932. // #11928
  933. test('should not lead to exponential perf cost with deeply chained computed', () => {
  934. const start = {
  935. prop1: shallowRef(1),
  936. prop2: shallowRef(2),
  937. prop3: shallowRef(3),
  938. prop4: shallowRef(4),
  939. }
  940. let layer = start
  941. const LAYERS = 1000
  942. for (let i = LAYERS; i > 0; i--) {
  943. const m = layer
  944. const s = {
  945. prop1: computed(() => m.prop2.value),
  946. prop2: computed(() => m.prop1.value - m.prop3.value),
  947. prop3: computed(() => m.prop2.value + m.prop4.value),
  948. prop4: computed(() => m.prop3.value),
  949. }
  950. effect(() => s.prop1.value)
  951. effect(() => s.prop2.value)
  952. effect(() => s.prop3.value)
  953. effect(() => s.prop4.value)
  954. s.prop1.value
  955. s.prop2.value
  956. s.prop3.value
  957. s.prop4.value
  958. layer = s
  959. }
  960. const t = performance.now()
  961. start.prop1.value = 4
  962. start.prop2.value = 3
  963. start.prop3.value = 2
  964. start.prop4.value = 1
  965. expect(performance.now() - t).toBeLessThan(process.env.CI ? 100 : 30)
  966. const end = layer
  967. expect([
  968. end.prop1.value,
  969. end.prop2.value,
  970. end.prop3.value,
  971. end.prop4.value,
  972. ]).toMatchObject([-2, -4, 2, 3])
  973. })
  974. test('performance when removing dependencies from deeply nested computeds', () => {
  975. const base = ref(1)
  976. const trigger = ref(true)
  977. const computeds: ComputedRef<number>[] = []
  978. const LAYERS = 30
  979. for (let i = 0; i < LAYERS; i++) {
  980. const earlier = [...computeds]
  981. computeds.push(
  982. computed(() => {
  983. return base.value + earlier.reduce((sum, c) => sum + c.value, 0)
  984. }),
  985. )
  986. }
  987. const tail = computed(() =>
  988. trigger.value ? computeds[computeds.length - 1].value : 0,
  989. )
  990. const t0 = performance.now()
  991. expect(tail.value).toBe(2 ** (LAYERS - 1))
  992. const t1 = performance.now()
  993. expect(t1 - t0).toBeLessThan(process.env.CI ? 100 : 30)
  994. trigger.value = false
  995. expect(tail.value).toBe(0)
  996. const t2 = performance.now()
  997. expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
  998. })
  999. })