computed.spec.ts 22 KB

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