computed.spec.ts 24 KB

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