computed.spec.ts 27 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  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 { ReactiveFlags } 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(c1.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
  414. expect(c2.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
  415. })
  416. it('should chained computeds dirtyLevel update with first computed effect', () => {
  417. const v = ref(0)
  418. const c1 = computed(() => {
  419. if (v.value === 0) {
  420. v.value = 1
  421. }
  422. return v.value
  423. })
  424. const c2 = computed(() => c1.value)
  425. const c3 = computed(() => c2.value)
  426. c3.value
  427. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  428. })
  429. it('should work when chained(ref+computed)', () => {
  430. const v = ref(0)
  431. const c1 = computed(() => {
  432. if (v.value === 0) {
  433. v.value = 1
  434. }
  435. return 'foo'
  436. })
  437. const c2 = computed(() => v.value + c1.value)
  438. expect(c2.value).toBe('0foo')
  439. expect(c2.value).toBe('1foo')
  440. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  441. })
  442. it('should trigger effect even computed already dirty', () => {
  443. const fnSpy = vi.fn()
  444. const v = ref(0)
  445. const c1 = computed(() => {
  446. if (v.value === 0) {
  447. v.value = 1
  448. }
  449. return 'foo'
  450. })
  451. const c2 = computed(() => v.value + c1.value)
  452. effect(() => {
  453. fnSpy(c2.value)
  454. })
  455. expect(fnSpy).toBeCalledTimes(1)
  456. expect(fnSpy.mock.calls).toMatchObject([['0foo']])
  457. expect(v.value).toBe(1)
  458. v.value = 2
  459. expect(fnSpy).toBeCalledTimes(2)
  460. expect(fnSpy.mock.calls).toMatchObject([['0foo'], ['2foo']])
  461. expect(v.value).toBe(2)
  462. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  463. })
  464. // #10185
  465. it('should not override queried MaybeDirty result', () => {
  466. class Item {
  467. v = ref(0)
  468. }
  469. const v1 = shallowRef()
  470. const v2 = ref(false)
  471. const c1 = computed(() => {
  472. let c = v1.value
  473. if (!v1.value) {
  474. c = new Item()
  475. v1.value = c
  476. }
  477. return c.v.value
  478. })
  479. const c2 = computed(() => {
  480. if (!v2.value) return 'no'
  481. return c1.value ? 'yes' : 'no'
  482. })
  483. const c3 = computed(() => c2.value)
  484. c3.value
  485. v2.value = true
  486. c3.value
  487. v1.value.v.value = 999
  488. expect(c3.value).toBe('yes')
  489. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  490. })
  491. it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
  492. const state = reactive<any>({})
  493. const consumer = computed(() => {
  494. if (!('a' in state)) state.a = 1
  495. return state.a
  496. })
  497. const Comp = {
  498. setup: () => {
  499. nextTick().then(() => {
  500. state.a = 2
  501. })
  502. return () => consumer.value
  503. },
  504. }
  505. const root = nodeOps.createElement('div')
  506. render(h(Comp), root)
  507. await nextTick()
  508. await nextTick()
  509. expect(serializeInner(root)).toBe(`2`)
  510. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  511. })
  512. it('should not trigger effect scheduler by recursive computed effect', async () => {
  513. const v = ref('Hello')
  514. const c = computed(() => {
  515. v.value += ' World'
  516. return v.value
  517. })
  518. const Comp = {
  519. setup: () => {
  520. return () => c.value
  521. },
  522. }
  523. const root = nodeOps.createElement('div')
  524. render(h(Comp), root)
  525. await nextTick()
  526. expect(serializeInner(root)).toBe('Hello World')
  527. v.value += ' World'
  528. await nextTick()
  529. expect(serializeInner(root)).toBe('Hello World World World World')
  530. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  531. })
  532. test('should not trigger if value did not change', () => {
  533. const src = ref(0)
  534. const c = computed(() => src.value % 2)
  535. const spy = vi.fn()
  536. effect(() => {
  537. spy(c.value)
  538. })
  539. expect(spy).toHaveBeenCalledTimes(1)
  540. src.value = 2
  541. // should not trigger
  542. expect(spy).toHaveBeenCalledTimes(1)
  543. src.value = 3
  544. src.value = 5
  545. // should trigger because latest value changes
  546. expect(spy).toHaveBeenCalledTimes(2)
  547. })
  548. test('chained computed trigger', () => {
  549. const effectSpy = vi.fn()
  550. const c1Spy = vi.fn()
  551. const c2Spy = vi.fn()
  552. const src = ref(0)
  553. const c1 = computed(() => {
  554. c1Spy()
  555. return src.value % 2
  556. })
  557. const c2 = computed(() => {
  558. c2Spy()
  559. return c1.value + 1
  560. })
  561. effect(() => {
  562. effectSpy(c2.value)
  563. })
  564. expect(c1Spy).toHaveBeenCalledTimes(1)
  565. expect(c2Spy).toHaveBeenCalledTimes(1)
  566. expect(effectSpy).toHaveBeenCalledTimes(1)
  567. src.value = 1
  568. expect(c1Spy).toHaveBeenCalledTimes(2)
  569. expect(c2Spy).toHaveBeenCalledTimes(2)
  570. expect(effectSpy).toHaveBeenCalledTimes(2)
  571. })
  572. test('chained computed avoid re-compute', () => {
  573. const effectSpy = vi.fn()
  574. const c1Spy = vi.fn()
  575. const c2Spy = vi.fn()
  576. const src = ref(0)
  577. const c1 = computed(() => {
  578. c1Spy()
  579. return src.value % 2
  580. })
  581. const c2 = computed(() => {
  582. c2Spy()
  583. return c1.value + 1
  584. })
  585. effect(() => {
  586. effectSpy(c2.value)
  587. })
  588. expect(effectSpy).toHaveBeenCalledTimes(1)
  589. src.value = 2
  590. src.value = 4
  591. src.value = 6
  592. expect(c1Spy).toHaveBeenCalledTimes(4)
  593. // c2 should not have to re-compute because c1 did not change.
  594. expect(c2Spy).toHaveBeenCalledTimes(1)
  595. // effect should not trigger because c2 did not change.
  596. expect(effectSpy).toHaveBeenCalledTimes(1)
  597. })
  598. test('chained computed value invalidation', () => {
  599. const effectSpy = vi.fn()
  600. const c1Spy = vi.fn()
  601. const c2Spy = vi.fn()
  602. const src = ref(0)
  603. const c1 = computed(() => {
  604. c1Spy()
  605. return src.value % 2
  606. })
  607. const c2 = computed(() => {
  608. c2Spy()
  609. return c1.value + 1
  610. })
  611. effect(() => {
  612. effectSpy(c2.value)
  613. })
  614. expect(effectSpy).toHaveBeenCalledTimes(1)
  615. expect(effectSpy).toHaveBeenCalledWith(1)
  616. expect(c2.value).toBe(1)
  617. expect(c1Spy).toHaveBeenCalledTimes(1)
  618. expect(c2Spy).toHaveBeenCalledTimes(1)
  619. src.value = 1
  620. // value should be available sync
  621. expect(c2.value).toBe(2)
  622. expect(c2Spy).toHaveBeenCalledTimes(2)
  623. })
  624. test('sync access of invalidated chained computed should not prevent final effect from running', () => {
  625. const effectSpy = vi.fn()
  626. const c1Spy = vi.fn()
  627. const c2Spy = vi.fn()
  628. const src = ref(0)
  629. const c1 = computed(() => {
  630. c1Spy()
  631. return src.value % 2
  632. })
  633. const c2 = computed(() => {
  634. c2Spy()
  635. return c1.value + 1
  636. })
  637. effect(() => {
  638. effectSpy(c2.value)
  639. })
  640. expect(effectSpy).toHaveBeenCalledTimes(1)
  641. src.value = 1
  642. // sync access c2
  643. c2.value
  644. expect(effectSpy).toHaveBeenCalledTimes(2)
  645. })
  646. it('computed should force track in untracked zone', () => {
  647. const n = ref(0)
  648. const spy1 = vi.fn()
  649. const spy2 = vi.fn()
  650. let c: ComputedRef
  651. effect(() => {
  652. spy1()
  653. pauseTracking()
  654. n.value
  655. c = computed(() => n.value + 1)
  656. // access computed now to force refresh
  657. c.value
  658. effect(() => spy2(c.value))
  659. n.value
  660. resetTracking()
  661. })
  662. expect(spy1).toHaveBeenCalledTimes(1)
  663. expect(spy2).toHaveBeenCalledTimes(1)
  664. n.value++
  665. // outer effect should not trigger
  666. expect(spy1).toHaveBeenCalledTimes(1)
  667. // inner effect should trigger
  668. expect(spy2).toHaveBeenCalledTimes(2)
  669. })
  670. // not recommended behavior, but needed for backwards compatibility
  671. // used in VueUse asyncComputed
  672. it('computed side effect should be able trigger', () => {
  673. const a = ref(false)
  674. const b = ref(false)
  675. const c = computed(() => {
  676. a.value = true
  677. return b.value
  678. })
  679. effect(() => {
  680. if (a.value) {
  681. b.value = true
  682. }
  683. })
  684. expect(b.value).toBe(false)
  685. // accessing c triggers change
  686. c.value
  687. expect(b.value).toBe(true)
  688. expect(c.value).toBe(true)
  689. })
  690. it('chained computed should work when accessed before having subs', () => {
  691. const n = ref(0)
  692. const c = computed(() => n.value)
  693. const d = computed(() => c.value + 1)
  694. const spy = vi.fn()
  695. // access
  696. d.value
  697. let dummy
  698. effect(() => {
  699. spy()
  700. dummy = d.value
  701. })
  702. expect(spy).toHaveBeenCalledTimes(1)
  703. expect(dummy).toBe(1)
  704. n.value++
  705. expect(spy).toHaveBeenCalledTimes(2)
  706. expect(dummy).toBe(2)
  707. })
  708. // #10236
  709. it('chained computed should still refresh after owner component unmount', async () => {
  710. const a = ref(0)
  711. const spy = vi.fn()
  712. const Child = {
  713. setup() {
  714. const b = computed(() => a.value + 1)
  715. const c = computed(() => b.value + 1)
  716. // access
  717. c.value
  718. onUnmounted(() => spy(c.value))
  719. return () => {}
  720. },
  721. }
  722. const show = ref(true)
  723. const Parent = {
  724. setup() {
  725. return () => (show.value ? h(Child) : null)
  726. },
  727. }
  728. render(h(Parent), nodeOps.createElement('div'))
  729. a.value++
  730. show.value = false
  731. await nextTick()
  732. expect(spy).toHaveBeenCalledWith(3)
  733. })
  734. // case: radix-vue `useForwardExpose` sets a template ref during mount,
  735. // and checks for the element's closest form element in a computed.
  736. // the computed is expected to only evaluate after mount.
  737. it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => {
  738. const calls: string[] = []
  739. const a = ref(0)
  740. const b = computed(() => {
  741. calls.push('b eval')
  742. return a.value + 1
  743. })
  744. const App = {
  745. setup() {
  746. onMounted(() => {
  747. calls.push('mounted')
  748. })
  749. return () =>
  750. h(
  751. 'div',
  752. {
  753. ref: () => (a.value = 1),
  754. },
  755. b.value,
  756. )
  757. },
  758. }
  759. render(h(App), nodeOps.createElement('div'))
  760. await nextTick()
  761. expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
  762. })
  763. it('should chained computeds keep reactivity when computed effect happens', async () => {
  764. const v = ref('Hello')
  765. const c = computed(() => {
  766. v.value += ' World'
  767. return v.value
  768. })
  769. const d = computed(() => c.value)
  770. const e = computed(() => d.value)
  771. const Comp = {
  772. setup: () => {
  773. return () => d.value + ' | ' + e.value
  774. },
  775. }
  776. const root = nodeOps.createElement('div')
  777. render(h(Comp), root)
  778. await nextTick()
  779. expect(serializeInner(root)).toBe('Hello World | Hello World')
  780. v.value += ' World'
  781. await nextTick()
  782. expect(serializeInner(root)).toBe(
  783. 'Hello World World World World | Hello World World World World',
  784. )
  785. })
  786. it('should keep dirty level when side effect computed value changed', () => {
  787. const v = ref(0)
  788. const c = computed(() => {
  789. v.value += 1
  790. return v.value
  791. })
  792. const d = computed(() => {
  793. return { d: c.value }
  794. })
  795. const Comp = {
  796. setup: () => {
  797. return () => {
  798. return [d.value.d, d.value.d]
  799. }
  800. },
  801. }
  802. const root = nodeOps.createElement('div')
  803. render(h(Comp), root)
  804. expect(d.value.d).toBe(1)
  805. expect(serializeInner(root)).toBe('11')
  806. })
  807. it('should be recomputed without being affected by side effects', () => {
  808. const v = ref(0)
  809. const c1 = computed(() => {
  810. v.value = 1
  811. return 0
  812. })
  813. const c2 = computed(() => {
  814. return v.value + ',' + c1.value
  815. })
  816. expect(c2.value).toBe('0,0')
  817. v.value = 1
  818. expect(c2.value).toBe('1,0')
  819. // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
  820. })
  821. it('debug: onTrigger (ref)', () => {
  822. let events: DebuggerEvent[] = []
  823. const onTrigger = vi.fn((e: DebuggerEvent) => {
  824. events.push(e)
  825. })
  826. const obj = ref(1)
  827. const c = computed(() => obj.value, { onTrigger })
  828. // computed won't track until it has a subscriber
  829. effect(() => c.value)
  830. obj.value++
  831. expect(c.value).toBe(2)
  832. expect(onTrigger).toHaveBeenCalledTimes(1)
  833. expect(events[0]).toEqual({
  834. effect: c,
  835. target: toRaw(obj),
  836. type: TriggerOpTypes.SET,
  837. key: 'value',
  838. oldValue: 1,
  839. newValue: 2,
  840. })
  841. })
  842. // #11797
  843. test('should prevent endless recursion in self-referencing computed getters', async () => {
  844. const Comp = defineComponent({
  845. data() {
  846. return {
  847. counter: 0,
  848. }
  849. },
  850. computed: {
  851. message(): string {
  852. if (this.counter === 0) {
  853. this.counter++
  854. return this.message
  855. } else {
  856. return `Step ${this.counter}`
  857. }
  858. },
  859. },
  860. render() {
  861. return [
  862. h(
  863. 'button',
  864. {
  865. onClick: () => {
  866. this.counter++
  867. },
  868. },
  869. 'Step',
  870. ),
  871. h('p', this.message),
  872. ]
  873. },
  874. })
  875. const root = nodeOps.createElement('div')
  876. render(h(Comp), root)
  877. expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 1</p>`)
  878. triggerEvent(root.children[1] as TestElement, 'click')
  879. await nextTick()
  880. expect(serializeInner(root)).toBe(`<button>Step</button><p>Step 2</p>`)
  881. })
  882. test('manual trigger computed', () => {
  883. const cValue = computed(() => 1)
  884. triggerRef(cValue)
  885. expect(cValue.value).toBe(1)
  886. })
  887. test('should not recompute if computed does not track reactive data', async () => {
  888. const spy = vi.fn()
  889. const c1 = computed(() => spy())
  890. c1.value
  891. ref(0).value++ // update globalVersion
  892. c1.value
  893. expect(spy).toBeCalledTimes(1)
  894. })
  895. test('computed should remain live after losing all subscribers', () => {
  896. const state = reactive({ a: 1 })
  897. const p = computed(() => state.a + 1)
  898. const { effect: e } = effect(() => p.value)
  899. e.stop()
  900. expect(p.value).toBe(2)
  901. state.a++
  902. expect(p.value).toBe(3)
  903. })
  904. // #11995
  905. test('computed dep cleanup should not cause property dep to be deleted', () => {
  906. const toggle = ref(true)
  907. const state = reactive({ a: 1 })
  908. const p = computed(() => {
  909. return toggle.value ? state.a : 111
  910. })
  911. const pp = computed(() => state.a)
  912. effect(() => p.value)
  913. expect(pp.value).toBe(1)
  914. toggle.value = false
  915. state.a++
  916. expect(pp.value).toBe(2)
  917. })
  918. // #12020
  919. test('computed value updates correctly after dep cleanup', () => {
  920. const obj = reactive({ foo: 1, flag: 1 })
  921. const c1 = computed(() => obj.foo)
  922. let foo
  923. effect(() => {
  924. foo = obj.flag ? (obj.foo, c1.value) : 0
  925. })
  926. expect(foo).toBe(1)
  927. obj.flag = 0
  928. expect(foo).toBe(0)
  929. obj.foo = 2
  930. obj.flag = 1
  931. expect(foo).toBe(2)
  932. })
  933. // #11928
  934. test('should not lead to exponential perf cost with deeply chained computed', () => {
  935. const start = {
  936. prop1: shallowRef(1),
  937. prop2: shallowRef(2),
  938. prop3: shallowRef(3),
  939. prop4: shallowRef(4),
  940. }
  941. let layer = start
  942. const LAYERS = 1000
  943. for (let i = LAYERS; i > 0; i--) {
  944. const m = layer
  945. const s = {
  946. prop1: computed(() => m.prop2.value),
  947. prop2: computed(() => m.prop1.value - m.prop3.value),
  948. prop3: computed(() => m.prop2.value + m.prop4.value),
  949. prop4: computed(() => m.prop3.value),
  950. }
  951. effect(() => s.prop1.value)
  952. effect(() => s.prop2.value)
  953. effect(() => s.prop3.value)
  954. effect(() => s.prop4.value)
  955. s.prop1.value
  956. s.prop2.value
  957. s.prop3.value
  958. s.prop4.value
  959. layer = s
  960. }
  961. const t = performance.now()
  962. start.prop1.value = 4
  963. start.prop2.value = 3
  964. start.prop3.value = 2
  965. start.prop4.value = 1
  966. expect(performance.now() - t).toBeLessThan(process.env.CI ? 100 : 30)
  967. const end = layer
  968. expect([
  969. end.prop1.value,
  970. end.prop2.value,
  971. end.prop3.value,
  972. end.prop4.value,
  973. ]).toMatchObject([-2, -4, 2, 3])
  974. })
  975. test('performance when removing dependencies from deeply nested computeds', () => {
  976. const base = ref(1)
  977. const trigger = ref(true)
  978. const computeds: ComputedRef<number>[] = []
  979. const LAYERS = 30
  980. for (let i = 0; i < LAYERS; i++) {
  981. const earlier = [...computeds]
  982. computeds.push(
  983. computed(() => {
  984. return base.value + earlier.reduce((sum, c) => sum + c.value, 0)
  985. }),
  986. )
  987. }
  988. const tail = computed(() =>
  989. trigger.value ? computeds[computeds.length - 1].value : 0,
  990. )
  991. const t0 = performance.now()
  992. expect(tail.value).toBe(2 ** (LAYERS - 1))
  993. const t1 = performance.now()
  994. expect(t1 - t0).toBeLessThan(process.env.CI ? 100 : 30)
  995. trigger.value = false
  996. expect(tail.value).toBe(0)
  997. const t2 = performance.now()
  998. expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30)
  999. })
  1000. })