apiWatch.spec.ts 35 KB

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