computed.spec.ts 26 KB

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