computed.spec.ts 27 KB

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