apiWatch.spec.ts 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520
  1. import {
  2. type ComponentInternalInstance,
  3. type ComponentPublicInstance,
  4. computed,
  5. defineComponent,
  6. getCurrentInstance,
  7. nextTick,
  8. reactive,
  9. ref,
  10. watch,
  11. watchEffect,
  12. } from '../src/index'
  13. import {
  14. type TestElement,
  15. createApp,
  16. h,
  17. nodeOps,
  18. onMounted,
  19. render,
  20. serializeInner,
  21. watchPostEffect,
  22. watchSyncEffect,
  23. } from '@vue/runtime-test'
  24. import {
  25. type DebuggerEvent,
  26. EffectFlags,
  27. ITERATE_KEY,
  28. type Ref,
  29. type ShallowRef,
  30. TrackOpTypes,
  31. TriggerOpTypes,
  32. effectScope,
  33. shallowReactive,
  34. shallowRef,
  35. toRef,
  36. triggerRef,
  37. } from '@vue/reactivity'
  38. // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch
  39. describe('api: watch', () => {
  40. it('effect', async () => {
  41. const state = reactive({ count: 0 })
  42. let dummy
  43. watchEffect(() => {
  44. dummy = state.count
  45. })
  46. expect(dummy).toBe(0)
  47. state.count++
  48. await nextTick()
  49. expect(dummy).toBe(1)
  50. })
  51. it('watching single source: getter', async () => {
  52. const state = reactive({ count: 0 })
  53. let dummy
  54. watch(
  55. () => state.count,
  56. (count, prevCount) => {
  57. dummy = [count, prevCount]
  58. // assert types
  59. count + 1
  60. if (prevCount) {
  61. prevCount + 1
  62. }
  63. },
  64. )
  65. state.count++
  66. await nextTick()
  67. expect(dummy).toMatchObject([1, 0])
  68. })
  69. it('watching single source: ref', async () => {
  70. const count = ref(0)
  71. let dummy
  72. watch(count, (count, prevCount) => {
  73. dummy = [count, prevCount]
  74. // assert types
  75. count + 1
  76. if (prevCount) {
  77. prevCount + 1
  78. }
  79. })
  80. count.value++
  81. await nextTick()
  82. expect(dummy).toMatchObject([1, 0])
  83. })
  84. it('watching single source: array', async () => {
  85. const array = reactive([] as number[])
  86. const spy = vi.fn()
  87. watch(array, spy)
  88. array.push(1)
  89. await nextTick()
  90. expect(spy).toBeCalledTimes(1)
  91. expect(spy).toBeCalledWith([1], [1], expect.anything())
  92. })
  93. it('should not fire if watched getter result did not change', async () => {
  94. const spy = vi.fn()
  95. const n = ref(0)
  96. watch(() => n.value % 2, spy)
  97. n.value++
  98. await nextTick()
  99. expect(spy).toBeCalledTimes(1)
  100. n.value += 2
  101. await nextTick()
  102. // should not be called again because getter result did not change
  103. expect(spy).toBeCalledTimes(1)
  104. })
  105. it('watching single source: computed ref', async () => {
  106. const count = ref(0)
  107. const plus = computed(() => count.value + 1)
  108. let dummy
  109. watch(plus, (count, prevCount) => {
  110. dummy = [count, prevCount]
  111. // assert types
  112. count + 1
  113. if (prevCount) {
  114. prevCount + 1
  115. }
  116. })
  117. count.value++
  118. await nextTick()
  119. expect(dummy).toMatchObject([2, 1])
  120. })
  121. it('watching primitive with deep: true', async () => {
  122. const count = ref(0)
  123. let dummy
  124. watch(
  125. count,
  126. (c, prevCount) => {
  127. dummy = [c, prevCount]
  128. },
  129. {
  130. deep: true,
  131. },
  132. )
  133. count.value++
  134. await nextTick()
  135. expect(dummy).toMatchObject([1, 0])
  136. })
  137. it('directly watching reactive object (with automatic deep: true)', async () => {
  138. const src = reactive({
  139. count: 0,
  140. })
  141. let dummy
  142. watch(src, ({ count }) => {
  143. dummy = count
  144. })
  145. src.count++
  146. await nextTick()
  147. expect(dummy).toBe(1)
  148. })
  149. it('directly watching reactive object with explicit deep: false', async () => {
  150. const src = reactive({
  151. state: {
  152. count: 0,
  153. },
  154. })
  155. let dummy
  156. watch(
  157. src,
  158. ({ state }) => {
  159. dummy = state?.count
  160. },
  161. {
  162. deep: false,
  163. },
  164. )
  165. // nested should not trigger
  166. src.state.count++
  167. await nextTick()
  168. expect(dummy).toBe(undefined)
  169. // root level should trigger
  170. src.state = { count: 1 }
  171. await nextTick()
  172. expect(dummy).toBe(1)
  173. })
  174. // #9916
  175. it('watching shallow reactive array with deep: false', async () => {
  176. class foo {
  177. prop1: ShallowRef<string> = shallowRef('')
  178. prop2: string = ''
  179. }
  180. const obj1 = new foo()
  181. const obj2 = new foo()
  182. const collection = shallowReactive([obj1, obj2])
  183. const cb = vi.fn()
  184. watch(collection, cb, { deep: false })
  185. collection[0].prop1.value = 'foo'
  186. await nextTick()
  187. // should not trigger
  188. expect(cb).toBeCalledTimes(0)
  189. collection.push(new foo())
  190. await nextTick()
  191. // should trigger on array self mutation
  192. expect(cb).toBeCalledTimes(1)
  193. })
  194. it('should still respect deep: true on shallowReactive source', async () => {
  195. const obj = reactive({ a: 1 })
  196. const arr = shallowReactive([obj])
  197. let dummy
  198. watch(
  199. arr,
  200. () => {
  201. dummy = arr[0].a
  202. },
  203. { deep: true },
  204. )
  205. obj.a++
  206. await nextTick()
  207. expect(dummy).toBe(2)
  208. })
  209. it('watching multiple sources', async () => {
  210. const state = reactive({ count: 1 })
  211. const count = ref(1)
  212. const plus = computed(() => count.value + 1)
  213. let dummy
  214. watch([() => state.count, count, plus], (vals, oldVals) => {
  215. dummy = [vals, oldVals]
  216. // assert types
  217. vals.concat(1)
  218. oldVals.concat(1)
  219. })
  220. state.count++
  221. count.value++
  222. await nextTick()
  223. expect(dummy).toMatchObject([
  224. [2, 2, 3],
  225. [1, 1, 2],
  226. ])
  227. })
  228. it('watching multiple sources: undefined initial values and immediate: true', async () => {
  229. const a = ref()
  230. const b = ref()
  231. let called = false
  232. watch(
  233. [a, b],
  234. ([newA, newB], [oldA, oldB]) => {
  235. called = true
  236. expect([newA, newB]).toMatchObject([undefined, undefined])
  237. expect([oldA, oldB]).toMatchObject([undefined, undefined])
  238. },
  239. { immediate: true },
  240. )
  241. await nextTick()
  242. expect(called).toBe(true)
  243. })
  244. it('watching multiple sources: readonly array', async () => {
  245. const state = reactive({ count: 1 })
  246. const status = ref(false)
  247. let dummy
  248. watch([() => state.count, status] as const, (vals, oldVals) => {
  249. dummy = [vals, oldVals]
  250. const [count] = vals
  251. const [, oldStatus] = oldVals
  252. // assert types
  253. count + 1
  254. oldStatus === true
  255. })
  256. state.count++
  257. status.value = true
  258. await nextTick()
  259. expect(dummy).toMatchObject([
  260. [2, true],
  261. [1, false],
  262. ])
  263. })
  264. it('watching multiple sources: reactive object (with automatic deep: true)', async () => {
  265. const src = reactive({ count: 0 })
  266. let dummy
  267. watch([src], ([state]) => {
  268. dummy = state
  269. // assert types
  270. state.count === 1
  271. })
  272. src.count++
  273. await nextTick()
  274. expect(dummy).toMatchObject({ count: 1 })
  275. })
  276. it('warn invalid watch source', () => {
  277. // @ts-expect-error
  278. watch(1, () => {})
  279. expect(`Invalid watch source`).toHaveBeenWarned()
  280. })
  281. it('warn invalid watch source: multiple sources', () => {
  282. watch([1], () => {})
  283. expect(`Invalid watch source`).toHaveBeenWarned()
  284. })
  285. it('stopping the watcher (effect)', async () => {
  286. const state = reactive({ count: 0 })
  287. let dummy
  288. const stop = watchEffect(() => {
  289. dummy = state.count
  290. })
  291. expect(dummy).toBe(0)
  292. stop()
  293. state.count++
  294. await nextTick()
  295. // should not update
  296. expect(dummy).toBe(0)
  297. })
  298. it('stopping the watcher (with source)', async () => {
  299. const state = reactive({ count: 0 })
  300. let dummy
  301. const stop = watch(
  302. () => state.count,
  303. count => {
  304. dummy = count
  305. },
  306. )
  307. state.count++
  308. await nextTick()
  309. expect(dummy).toBe(1)
  310. stop()
  311. state.count++
  312. await nextTick()
  313. // should not update
  314. expect(dummy).toBe(1)
  315. })
  316. it('cleanup registration (effect)', async () => {
  317. const state = reactive({ count: 0 })
  318. const cleanup = vi.fn()
  319. let dummy
  320. const stop = watchEffect(onCleanup => {
  321. onCleanup(cleanup)
  322. dummy = state.count
  323. })
  324. expect(dummy).toBe(0)
  325. state.count++
  326. await nextTick()
  327. expect(cleanup).toHaveBeenCalledTimes(1)
  328. expect(dummy).toBe(1)
  329. stop()
  330. expect(cleanup).toHaveBeenCalledTimes(2)
  331. })
  332. it('cleanup registration (with source)', async () => {
  333. const count = ref(0)
  334. const cleanup = vi.fn()
  335. let dummy
  336. const stop = watch(count, (count, prevCount, onCleanup) => {
  337. onCleanup(cleanup)
  338. dummy = count
  339. })
  340. count.value++
  341. await nextTick()
  342. expect(cleanup).toHaveBeenCalledTimes(0)
  343. expect(dummy).toBe(1)
  344. count.value++
  345. await nextTick()
  346. expect(cleanup).toHaveBeenCalledTimes(1)
  347. expect(dummy).toBe(2)
  348. stop()
  349. expect(cleanup).toHaveBeenCalledTimes(2)
  350. })
  351. it('flush timing: pre (default)', async () => {
  352. const count = ref(0)
  353. const count2 = ref(0)
  354. let callCount = 0
  355. let result1
  356. let result2
  357. const assertion = vi.fn((count, count2Value) => {
  358. callCount++
  359. // on mount, the watcher callback should be called before DOM render
  360. // on update, should be called before the count is updated
  361. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  362. result1 = serializeInner(root) === expectedDOM
  363. // in a pre-flush callback, all state should have been updated
  364. const expectedState = callCount - 1
  365. result2 = count === expectedState && count2Value === expectedState
  366. })
  367. const Comp = {
  368. setup() {
  369. watchEffect(() => {
  370. assertion(count.value, count2.value)
  371. })
  372. return () => count.value
  373. },
  374. }
  375. const root = nodeOps.createElement('div')
  376. render(h(Comp), root)
  377. expect(assertion).toHaveBeenCalledTimes(1)
  378. expect(result1).toBe(true)
  379. expect(result2).toBe(true)
  380. count.value++
  381. count2.value++
  382. await nextTick()
  383. // two mutations should result in 1 callback execution
  384. expect(assertion).toHaveBeenCalledTimes(2)
  385. expect(result1).toBe(true)
  386. expect(result2).toBe(true)
  387. })
  388. it('flush timing: post', async () => {
  389. const count = ref(0)
  390. let result
  391. const assertion = vi.fn(count => {
  392. result = serializeInner(root) === `${count}`
  393. })
  394. const Comp = {
  395. setup() {
  396. watchEffect(
  397. () => {
  398. assertion(count.value)
  399. },
  400. { flush: 'post' },
  401. )
  402. return () => count.value
  403. },
  404. }
  405. const root = nodeOps.createElement('div')
  406. render(h(Comp), root)
  407. expect(assertion).toHaveBeenCalledTimes(1)
  408. expect(result).toBe(true)
  409. count.value++
  410. await nextTick()
  411. expect(assertion).toHaveBeenCalledTimes(2)
  412. expect(result).toBe(true)
  413. })
  414. it('watchPostEffect', async () => {
  415. const count = ref(0)
  416. let result
  417. const assertion = vi.fn(count => {
  418. result = serializeInner(root) === `${count}`
  419. })
  420. const Comp = {
  421. setup() {
  422. watchPostEffect(() => {
  423. assertion(count.value)
  424. })
  425. return () => count.value
  426. },
  427. }
  428. const root = nodeOps.createElement('div')
  429. render(h(Comp), root)
  430. expect(assertion).toHaveBeenCalledTimes(1)
  431. expect(result).toBe(true)
  432. count.value++
  433. await nextTick()
  434. expect(assertion).toHaveBeenCalledTimes(2)
  435. expect(result).toBe(true)
  436. })
  437. it('flush timing: sync', async () => {
  438. const count = ref(0)
  439. const count2 = ref(0)
  440. let callCount = 0
  441. let result1
  442. let result2
  443. const assertion = vi.fn(count => {
  444. callCount++
  445. // on mount, the watcher callback should be called before DOM render
  446. // on update, should be called before the count is updated
  447. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  448. result1 = serializeInner(root) === expectedDOM
  449. // in a sync callback, state mutation on the next line should not have
  450. // executed yet on the 2nd call, but will be on the 3rd call.
  451. const expectedState = callCount < 3 ? 0 : 1
  452. result2 = count2.value === expectedState
  453. })
  454. const Comp = {
  455. setup() {
  456. watchEffect(
  457. () => {
  458. assertion(count.value)
  459. },
  460. {
  461. flush: 'sync',
  462. },
  463. )
  464. return () => count.value
  465. },
  466. }
  467. const root = nodeOps.createElement('div')
  468. render(h(Comp), root)
  469. expect(assertion).toHaveBeenCalledTimes(1)
  470. expect(result1).toBe(true)
  471. expect(result2).toBe(true)
  472. count.value++
  473. count2.value++
  474. await nextTick()
  475. expect(assertion).toHaveBeenCalledTimes(3)
  476. expect(result1).toBe(true)
  477. expect(result2).toBe(true)
  478. })
  479. it('watchSyncEffect', async () => {
  480. const count = ref(0)
  481. const count2 = ref(0)
  482. let callCount = 0
  483. let result1
  484. let result2
  485. const assertion = vi.fn(count => {
  486. callCount++
  487. // on mount, the watcher callback should be called before DOM render
  488. // on update, should be called before the count is updated
  489. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  490. result1 = serializeInner(root) === expectedDOM
  491. // in a sync callback, state mutation on the next line should not have
  492. // executed yet on the 2nd call, but will be on the 3rd call.
  493. const expectedState = callCount < 3 ? 0 : 1
  494. result2 = count2.value === expectedState
  495. })
  496. const Comp = {
  497. setup() {
  498. watchSyncEffect(() => {
  499. assertion(count.value)
  500. })
  501. return () => count.value
  502. },
  503. }
  504. const root = nodeOps.createElement('div')
  505. render(h(Comp), root)
  506. expect(assertion).toHaveBeenCalledTimes(1)
  507. expect(result1).toBe(true)
  508. expect(result2).toBe(true)
  509. count.value++
  510. count2.value++
  511. await nextTick()
  512. expect(assertion).toHaveBeenCalledTimes(3)
  513. expect(result1).toBe(true)
  514. expect(result2).toBe(true)
  515. })
  516. it('should not fire on component unmount w/ flush: post', async () => {
  517. const toggle = ref(true)
  518. const cb = vi.fn()
  519. const Comp = {
  520. setup() {
  521. watch(toggle, cb, { flush: 'post' })
  522. },
  523. render() {},
  524. }
  525. const App = {
  526. render() {
  527. return toggle.value ? h(Comp) : null
  528. },
  529. }
  530. render(h(App), nodeOps.createElement('div'))
  531. expect(cb).not.toHaveBeenCalled()
  532. toggle.value = false
  533. await nextTick()
  534. expect(cb).not.toHaveBeenCalled()
  535. })
  536. // #2291
  537. it('should not fire on component unmount w/ flush: pre', async () => {
  538. const toggle = ref(true)
  539. const cb = vi.fn()
  540. const Comp = {
  541. setup() {
  542. watch(toggle, cb, { flush: 'pre' })
  543. },
  544. render() {},
  545. }
  546. const App = {
  547. render() {
  548. return toggle.value ? h(Comp) : null
  549. },
  550. }
  551. render(h(App), nodeOps.createElement('div'))
  552. expect(cb).not.toHaveBeenCalled()
  553. toggle.value = false
  554. await nextTick()
  555. expect(cb).not.toHaveBeenCalled()
  556. })
  557. // #7030
  558. it('should not fire on child component unmount w/ flush: pre', async () => {
  559. const visible = ref(true)
  560. const cb = vi.fn()
  561. const Parent = defineComponent({
  562. props: ['visible'],
  563. render() {
  564. return visible.value ? h(Comp) : null
  565. },
  566. })
  567. const Comp = {
  568. setup() {
  569. watch(visible, cb, { flush: 'pre' })
  570. },
  571. render() {},
  572. }
  573. const App = {
  574. render() {
  575. return h(Parent, {
  576. visible: visible.value,
  577. })
  578. },
  579. }
  580. render(h(App), nodeOps.createElement('div'))
  581. expect(cb).not.toHaveBeenCalled()
  582. visible.value = false
  583. await nextTick()
  584. expect(cb).not.toHaveBeenCalled()
  585. })
  586. // #7030
  587. it('flush: pre watcher in child component should not fire before parent update', async () => {
  588. const b = ref(0)
  589. const calls: string[] = []
  590. const Comp = {
  591. setup() {
  592. watch(
  593. () => b.value,
  594. val => {
  595. calls.push('watcher child')
  596. },
  597. { flush: 'pre' },
  598. )
  599. return () => {
  600. b.value
  601. calls.push('render child')
  602. }
  603. },
  604. }
  605. const Parent = {
  606. props: ['a'],
  607. setup() {
  608. watch(
  609. () => b.value,
  610. val => {
  611. calls.push('watcher parent')
  612. },
  613. { flush: 'pre' },
  614. )
  615. return () => {
  616. b.value
  617. calls.push('render parent')
  618. return h(Comp)
  619. }
  620. },
  621. }
  622. const App = {
  623. render() {
  624. return h(Parent, {
  625. a: b.value,
  626. })
  627. },
  628. }
  629. render(h(App), nodeOps.createElement('div'))
  630. expect(calls).toEqual(['render parent', 'render child'])
  631. b.value++
  632. await nextTick()
  633. expect(calls).toEqual([
  634. 'render parent',
  635. 'render child',
  636. 'watcher parent',
  637. 'render parent',
  638. 'watcher child',
  639. 'render child',
  640. ])
  641. })
  642. // #1763
  643. it('flush: pre watcher watching props should fire before child update', async () => {
  644. const a = ref(0)
  645. const b = ref(0)
  646. const c = ref(0)
  647. const calls: string[] = []
  648. const Comp = {
  649. props: ['a', 'b'],
  650. setup(props: any) {
  651. watch(
  652. () => props.a + props.b,
  653. () => {
  654. calls.push('watcher 1')
  655. c.value++
  656. },
  657. { flush: 'pre' },
  658. )
  659. // #1777 chained pre-watcher
  660. watch(
  661. c,
  662. () => {
  663. calls.push('watcher 2')
  664. },
  665. { flush: 'pre' },
  666. )
  667. return () => {
  668. c.value
  669. calls.push('render')
  670. }
  671. },
  672. }
  673. const App = {
  674. render() {
  675. return h(Comp, { a: a.value, b: b.value })
  676. },
  677. }
  678. render(h(App), nodeOps.createElement('div'))
  679. expect(calls).toEqual(['render'])
  680. // both props are updated
  681. // should trigger pre-flush watcher first and only once
  682. // then trigger child render
  683. a.value++
  684. b.value++
  685. await nextTick()
  686. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  687. })
  688. // #5721
  689. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  690. const count = ref(0)
  691. const calls: string[] = []
  692. const App = {
  693. render() {},
  694. setup() {
  695. watch(
  696. count,
  697. () => {
  698. calls.push('watch ' + count.value)
  699. },
  700. { flush: 'pre' },
  701. )
  702. onMounted(() => {
  703. calls.push('mounted')
  704. })
  705. // mutate multiple times
  706. count.value++
  707. count.value++
  708. count.value++
  709. },
  710. }
  711. render(h(App), nodeOps.createElement('div'))
  712. expect(calls).toMatchObject(['watch 3', 'mounted'])
  713. })
  714. // #1852
  715. it('flush: post watcher should fire after template refs updated', async () => {
  716. const toggle = ref(false)
  717. let dom: TestElement | null = null
  718. const App = {
  719. setup() {
  720. const domRef = ref<TestElement | null>(null)
  721. watch(
  722. toggle,
  723. () => {
  724. dom = domRef.value
  725. },
  726. { flush: 'post' },
  727. )
  728. return () => {
  729. return toggle.value ? h('p', { ref: domRef }) : null
  730. }
  731. },
  732. }
  733. render(h(App), nodeOps.createElement('div'))
  734. expect(dom).toBe(null)
  735. toggle.value = true
  736. await nextTick()
  737. expect(dom!.tag).toBe('p')
  738. })
  739. it('deep', async () => {
  740. const state = reactive({
  741. nested: {
  742. count: ref(0),
  743. },
  744. array: [1, 2, 3],
  745. map: new Map([
  746. ['a', 1],
  747. ['b', 2],
  748. ]),
  749. set: new Set([1, 2, 3]),
  750. })
  751. let dummy
  752. watch(
  753. () => state,
  754. state => {
  755. dummy = [
  756. state.nested.count,
  757. state.array[0],
  758. state.map.get('a'),
  759. state.set.has(1),
  760. ]
  761. },
  762. { deep: true },
  763. )
  764. state.nested.count++
  765. await nextTick()
  766. expect(dummy).toEqual([1, 1, 1, true])
  767. // nested array mutation
  768. state.array[0] = 2
  769. await nextTick()
  770. expect(dummy).toEqual([1, 2, 1, true])
  771. // nested map mutation
  772. state.map.set('a', 2)
  773. await nextTick()
  774. expect(dummy).toEqual([1, 2, 2, true])
  775. // nested set mutation
  776. state.set.delete(1)
  777. await nextTick()
  778. expect(dummy).toEqual([1, 2, 2, false])
  779. })
  780. it('watching deep ref', async () => {
  781. const count = ref(0)
  782. const double = computed(() => count.value * 2)
  783. const state = reactive([count, double])
  784. let dummy
  785. watch(
  786. () => state,
  787. state => {
  788. dummy = [state[0].value, state[1].value]
  789. },
  790. { deep: true },
  791. )
  792. count.value++
  793. await nextTick()
  794. expect(dummy).toEqual([1, 2])
  795. })
  796. it('immediate', async () => {
  797. const count = ref(0)
  798. const cb = vi.fn()
  799. watch(count, cb, { immediate: true })
  800. expect(cb).toHaveBeenCalledTimes(1)
  801. count.value++
  802. await nextTick()
  803. expect(cb).toHaveBeenCalledTimes(2)
  804. })
  805. it('immediate: triggers when initial value is null', async () => {
  806. const state = ref(null)
  807. const spy = vi.fn()
  808. watch(() => state.value, spy, { immediate: true })
  809. expect(spy).toHaveBeenCalled()
  810. })
  811. it('immediate: triggers when initial value is undefined', async () => {
  812. const state = ref()
  813. const spy = vi.fn()
  814. watch(() => state.value, spy, { immediate: true })
  815. expect(spy).toHaveBeenCalledWith(undefined, undefined, expect.any(Function))
  816. state.value = 3
  817. await nextTick()
  818. expect(spy).toHaveBeenCalledTimes(2)
  819. // testing if undefined can trigger the watcher
  820. state.value = undefined
  821. await nextTick()
  822. expect(spy).toHaveBeenCalledTimes(3)
  823. // it shouldn't trigger if the same value is set
  824. state.value = undefined
  825. await nextTick()
  826. expect(spy).toHaveBeenCalledTimes(3)
  827. })
  828. it('warn immediate option when using effect', async () => {
  829. const count = ref(0)
  830. let dummy
  831. watchEffect(
  832. () => {
  833. dummy = count.value
  834. },
  835. // @ts-expect-error
  836. { immediate: false },
  837. )
  838. expect(dummy).toBe(0)
  839. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  840. count.value++
  841. await nextTick()
  842. expect(dummy).toBe(1)
  843. })
  844. it('warn and not respect deep option when using effect', async () => {
  845. const arr = ref([1, [2]])
  846. const spy = vi.fn()
  847. watchEffect(
  848. () => {
  849. spy()
  850. return arr
  851. },
  852. // @ts-expect-error
  853. { deep: true },
  854. )
  855. expect(spy).toHaveBeenCalledTimes(1)
  856. ;(arr.value[1] as Array<number>)[0] = 3
  857. await nextTick()
  858. expect(spy).toHaveBeenCalledTimes(1)
  859. expect(`"deep" option is only respected`).toHaveBeenWarned()
  860. })
  861. it('onTrack', async () => {
  862. const events: DebuggerEvent[] = []
  863. let dummy
  864. const onTrack = vi.fn((e: DebuggerEvent) => {
  865. events.push(e)
  866. })
  867. const obj = reactive({ foo: 1, bar: 2 })
  868. watchEffect(
  869. () => {
  870. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  871. },
  872. { onTrack },
  873. )
  874. await nextTick()
  875. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  876. expect(onTrack).toHaveBeenCalledTimes(3)
  877. expect(events).toMatchObject([
  878. {
  879. target: obj,
  880. type: TrackOpTypes.GET,
  881. key: 'foo',
  882. },
  883. {
  884. target: obj,
  885. type: TrackOpTypes.HAS,
  886. key: 'bar',
  887. },
  888. {
  889. target: obj,
  890. type: TrackOpTypes.ITERATE,
  891. key: ITERATE_KEY,
  892. },
  893. ])
  894. })
  895. it('onTrigger', async () => {
  896. const events: DebuggerEvent[] = []
  897. let dummy
  898. const onTrigger = vi.fn((e: DebuggerEvent) => {
  899. events.push(e)
  900. })
  901. const obj = reactive<{ foo?: number }>({ foo: 1 })
  902. watchEffect(
  903. () => {
  904. dummy = obj.foo
  905. },
  906. { onTrigger },
  907. )
  908. await nextTick()
  909. expect(dummy).toBe(1)
  910. obj.foo!++
  911. await nextTick()
  912. expect(dummy).toBe(2)
  913. expect(onTrigger).toHaveBeenCalledTimes(1)
  914. expect(events[0]).toMatchObject({
  915. type: TriggerOpTypes.SET,
  916. key: 'foo',
  917. oldValue: 1,
  918. newValue: 2,
  919. })
  920. delete obj.foo
  921. await nextTick()
  922. expect(dummy).toBeUndefined()
  923. expect(onTrigger).toHaveBeenCalledTimes(2)
  924. expect(events[1]).toMatchObject({
  925. type: TriggerOpTypes.DELETE,
  926. key: 'foo',
  927. oldValue: 2,
  928. })
  929. })
  930. it('should work sync', () => {
  931. const v = ref(1)
  932. let calls = 0
  933. watch(
  934. v,
  935. () => {
  936. ++calls
  937. },
  938. {
  939. flush: 'sync',
  940. },
  941. )
  942. expect(calls).toBe(0)
  943. v.value++
  944. expect(calls).toBe(1)
  945. })
  946. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  947. const v = shallowRef({ a: 1 })
  948. let sideEffect = 0
  949. watch(v, obj => {
  950. sideEffect = obj.a
  951. })
  952. v.value = v.value
  953. await nextTick()
  954. // should not trigger
  955. expect(sideEffect).toBe(0)
  956. v.value.a++
  957. await nextTick()
  958. // should not trigger
  959. expect(sideEffect).toBe(0)
  960. triggerRef(v)
  961. await nextTick()
  962. // should trigger now
  963. expect(sideEffect).toBe(2)
  964. })
  965. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  966. const v = shallowRef([] as any)
  967. const spy = vi.fn()
  968. watch([v], () => {
  969. spy()
  970. })
  971. v.value.push(1)
  972. triggerRef(v)
  973. await nextTick()
  974. // should trigger now
  975. expect(spy).toHaveBeenCalledTimes(1)
  976. })
  977. test('should force trigger on triggerRef with toRef from reactive', async () => {
  978. const foo = reactive({ bar: 1 })
  979. const bar = toRef(foo, 'bar')
  980. const spy = vi.fn()
  981. watchEffect(() => {
  982. bar.value
  983. spy()
  984. })
  985. expect(spy).toHaveBeenCalledTimes(1)
  986. triggerRef(bar)
  987. await nextTick()
  988. // should trigger now
  989. expect(spy).toHaveBeenCalledTimes(2)
  990. })
  991. // #2125
  992. test('watchEffect should not recursively trigger itself', async () => {
  993. const spy = vi.fn()
  994. const price = ref(10)
  995. const history = ref<number[]>([])
  996. watchEffect(() => {
  997. history.value.push(price.value)
  998. spy()
  999. })
  1000. await nextTick()
  1001. expect(spy).toHaveBeenCalledTimes(1)
  1002. })
  1003. // #2231
  1004. test('computed refs should not trigger watch if value has no change', async () => {
  1005. const spy = vi.fn()
  1006. const source = ref(0)
  1007. const price = computed(() => source.value === 0)
  1008. watch(price, spy)
  1009. source.value++
  1010. await nextTick()
  1011. source.value++
  1012. await nextTick()
  1013. expect(spy).toHaveBeenCalledTimes(1)
  1014. })
  1015. // https://github.com/vuejs/core/issues/2381
  1016. test('$watch should always register its effects with its own instance', async () => {
  1017. let instance: ComponentInternalInstance | null
  1018. let _show: Ref<boolean>
  1019. const Child = defineComponent({
  1020. render: () => h('div'),
  1021. mounted() {
  1022. instance = getCurrentInstance()
  1023. },
  1024. unmounted() {},
  1025. })
  1026. const Comp = defineComponent({
  1027. setup() {
  1028. const comp = ref<ComponentPublicInstance | undefined>()
  1029. const show = ref(true)
  1030. _show = show
  1031. return { comp, show }
  1032. },
  1033. render() {
  1034. return this.show
  1035. ? h(Child, {
  1036. ref: vm => void (this.comp = vm as ComponentPublicInstance),
  1037. })
  1038. : null
  1039. },
  1040. mounted() {
  1041. // this call runs while Comp is currentInstance, but
  1042. // the effect for this `$watch` should nonetheless be registered with Child
  1043. this.comp!.$watch(
  1044. () => this.show,
  1045. () => void 0,
  1046. )
  1047. },
  1048. })
  1049. render(h(Comp), nodeOps.createElement('div'))
  1050. expect(instance!).toBeDefined()
  1051. expect(instance!.scope.effects).toBeInstanceOf(Array)
  1052. // includes the component's own render effect AND the watcher effect
  1053. expect(instance!.scope.effects.length).toBe(2)
  1054. _show!.value = false
  1055. await nextTick()
  1056. await nextTick()
  1057. expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
  1058. })
  1059. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  1060. let instance: any
  1061. const source = vi.fn()
  1062. const Comp = defineComponent({
  1063. render() {},
  1064. created(this: any) {
  1065. instance = this
  1066. this.$watch(source, function () {})
  1067. },
  1068. })
  1069. const root = nodeOps.createElement('div')
  1070. createApp(Comp).mount(root)
  1071. expect(instance).toBeDefined()
  1072. expect(source.mock.calls.some(args => args.includes(instance)))
  1073. })
  1074. test('should not leak `this.proxy` to setup()', () => {
  1075. const source = vi.fn()
  1076. const Comp = defineComponent({
  1077. render() {},
  1078. setup() {
  1079. watch(source, () => {})
  1080. },
  1081. })
  1082. const root = nodeOps.createElement('div')
  1083. createApp(Comp).mount(root)
  1084. // should not have any arguments
  1085. expect(source.mock.calls[0]).toMatchObject([])
  1086. })
  1087. // #2728
  1088. test('pre watcher callbacks should not track dependencies', async () => {
  1089. const a = ref(0)
  1090. const b = ref(0)
  1091. const updated = vi.fn()
  1092. const Child = defineComponent({
  1093. props: ['a'],
  1094. updated,
  1095. watch: {
  1096. a() {
  1097. b.value
  1098. },
  1099. },
  1100. render() {
  1101. return h('div', this.a)
  1102. },
  1103. })
  1104. const Parent = defineComponent({
  1105. render() {
  1106. return h(Child, { a: a.value })
  1107. },
  1108. })
  1109. const root = nodeOps.createElement('div')
  1110. createApp(Parent).mount(root)
  1111. a.value++
  1112. await nextTick()
  1113. expect(updated).toHaveBeenCalledTimes(1)
  1114. b.value++
  1115. await nextTick()
  1116. // should not track b as dependency of Child
  1117. expect(updated).toHaveBeenCalledTimes(1)
  1118. })
  1119. test('watching keypath', async () => {
  1120. const spy = vi.fn()
  1121. const Comp = defineComponent({
  1122. render() {},
  1123. data() {
  1124. return {
  1125. a: {
  1126. b: 1,
  1127. },
  1128. }
  1129. },
  1130. watch: {
  1131. 'a.b': spy,
  1132. },
  1133. created(this: any) {
  1134. this.$watch('a.b', spy)
  1135. },
  1136. mounted(this: any) {
  1137. this.a.b++
  1138. },
  1139. })
  1140. const root = nodeOps.createElement('div')
  1141. createApp(Comp).mount(root)
  1142. await nextTick()
  1143. expect(spy).toHaveBeenCalledTimes(2)
  1144. })
  1145. it('watching sources: ref<any[]>', async () => {
  1146. const foo = ref([1])
  1147. const spy = vi.fn()
  1148. watch(foo, () => {
  1149. spy()
  1150. })
  1151. foo.value = foo.value.slice()
  1152. await nextTick()
  1153. expect(spy).toBeCalledTimes(1)
  1154. })
  1155. it('watching multiple sources: computed', async () => {
  1156. let count = 0
  1157. const value = ref('1')
  1158. const plus = computed(() => !!value.value)
  1159. watch([plus], () => {
  1160. count++
  1161. })
  1162. value.value = '2'
  1163. await nextTick()
  1164. expect(plus.value).toBe(true)
  1165. expect(count).toBe(0)
  1166. })
  1167. // #4158
  1168. test('watch should not register in owner component if created inside detached scope', () => {
  1169. let instance: ComponentInternalInstance
  1170. const Comp = {
  1171. setup() {
  1172. instance = getCurrentInstance()!
  1173. effectScope(true).run(() => {
  1174. watch(
  1175. () => 1,
  1176. () => {},
  1177. )
  1178. })
  1179. return () => ''
  1180. },
  1181. }
  1182. const root = nodeOps.createElement('div')
  1183. createApp(Comp).mount(root)
  1184. // should not record watcher in detached scope and only the instance's
  1185. // own update effect
  1186. expect(instance!.scope.effects.length).toBe(1)
  1187. })
  1188. test('watchEffect should keep running if created in a detached scope', async () => {
  1189. const trigger = ref(0)
  1190. let countWE = 0
  1191. let countW = 0
  1192. const Comp = {
  1193. setup() {
  1194. effectScope(true).run(() => {
  1195. watchEffect(() => {
  1196. trigger.value
  1197. countWE++
  1198. })
  1199. watch(trigger, () => countW++)
  1200. })
  1201. return () => ''
  1202. },
  1203. }
  1204. const root = nodeOps.createElement('div')
  1205. render(h(Comp), root)
  1206. // only watchEffect as ran so far
  1207. expect(countWE).toBe(1)
  1208. expect(countW).toBe(0)
  1209. trigger.value++
  1210. await nextTick()
  1211. // both watchers run while component is mounted
  1212. expect(countWE).toBe(2)
  1213. expect(countW).toBe(1)
  1214. render(null, root) // unmount
  1215. await nextTick()
  1216. trigger.value++
  1217. await nextTick()
  1218. // both watchers run again event though component has been unmounted
  1219. expect(countWE).toBe(3)
  1220. expect(countW).toBe(2)
  1221. })
  1222. const options = [
  1223. { name: 'only trigger once watch' },
  1224. {
  1225. deep: true,
  1226. name: 'only trigger once watch with deep',
  1227. },
  1228. {
  1229. flush: 'sync',
  1230. name: 'only trigger once watch with flush: sync',
  1231. },
  1232. {
  1233. flush: 'pre',
  1234. name: 'only trigger once watch with flush: pre',
  1235. },
  1236. {
  1237. immediate: true,
  1238. name: 'only trigger once watch with immediate',
  1239. },
  1240. ] as const
  1241. test.each(options)('$name', async option => {
  1242. const count = ref(0)
  1243. const cb = vi.fn()
  1244. watch(count, cb, { once: true, ...option })
  1245. count.value++
  1246. await nextTick()
  1247. expect(count.value).toBe(1)
  1248. expect(cb).toHaveBeenCalledTimes(1)
  1249. count.value++
  1250. await nextTick()
  1251. expect(count.value).toBe(2)
  1252. expect(cb).toHaveBeenCalledTimes(1)
  1253. })
  1254. // #5151
  1255. test('OnCleanup also needs to be cleaned,', async () => {
  1256. const spy1 = vi.fn()
  1257. const spy2 = vi.fn()
  1258. const num = ref(0)
  1259. watch(num, (value, oldValue, onCleanup) => {
  1260. if (value > 1) {
  1261. return
  1262. }
  1263. spy1()
  1264. onCleanup(() => {
  1265. // OnCleanup also needs to be cleaned
  1266. spy2()
  1267. })
  1268. })
  1269. num.value++
  1270. await nextTick()
  1271. expect(spy1).toHaveBeenCalledTimes(1)
  1272. expect(spy2).toHaveBeenCalledTimes(0)
  1273. num.value++
  1274. await nextTick()
  1275. expect(spy1).toHaveBeenCalledTimes(1)
  1276. expect(spy2).toHaveBeenCalledTimes(1)
  1277. num.value++
  1278. await nextTick()
  1279. // would not be calld when value>1
  1280. expect(spy1).toHaveBeenCalledTimes(1)
  1281. expect(spy2).toHaveBeenCalledTimes(1)
  1282. })
  1283. test("effect should be removed from scope's effects after it is stopped", () => {
  1284. const num = ref(0)
  1285. let unwatch: () => void
  1286. let instance: ComponentInternalInstance
  1287. const Comp = {
  1288. setup() {
  1289. instance = getCurrentInstance()!
  1290. unwatch = watch(num, () => {
  1291. console.log(num.value)
  1292. })
  1293. return () => null
  1294. },
  1295. }
  1296. const root = nodeOps.createElement('div')
  1297. createApp(Comp).mount(root)
  1298. expect(instance!.scope.effects.length).toBe(2)
  1299. unwatch!()
  1300. expect(instance!.scope.effects.length).toBe(1)
  1301. const scope = effectScope()
  1302. scope.run(() => {
  1303. unwatch = watch(num, () => {
  1304. console.log(num.value)
  1305. })
  1306. })
  1307. expect(scope.effects.length).toBe(1)
  1308. unwatch!()
  1309. expect(scope.effects.length).toBe(0)
  1310. })
  1311. // simplified case of VueUse syncRef
  1312. test('sync watcher should not be batched', () => {
  1313. const a = ref(0)
  1314. const b = ref(0)
  1315. let pauseB = false
  1316. watch(
  1317. a,
  1318. () => {
  1319. pauseB = true
  1320. b.value = a.value + 1
  1321. pauseB = false
  1322. },
  1323. { flush: 'sync' },
  1324. )
  1325. watch(
  1326. b,
  1327. () => {
  1328. if (!pauseB) {
  1329. throw new Error('should not be called')
  1330. }
  1331. },
  1332. { flush: 'sync' },
  1333. )
  1334. a.value = 1
  1335. expect(b.value).toBe(2)
  1336. })
  1337. test('watchEffect should not fire on computed deps that did not change', async () => {
  1338. const a = ref(0)
  1339. const c = computed(() => a.value % 2)
  1340. const spy = vi.fn()
  1341. watchEffect(() => {
  1342. spy()
  1343. c.value
  1344. })
  1345. expect(spy).toHaveBeenCalledTimes(1)
  1346. a.value += 2
  1347. await nextTick()
  1348. expect(spy).toHaveBeenCalledTimes(1)
  1349. })
  1350. })