apiWatch.spec.ts 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507
  1. import {
  2. type ComponentInternalInstance,
  3. type ComponentPublicInstance,
  4. computed,
  5. defineComponent,
  6. getCurrentInstance,
  7. nextTick,
  8. onEffectCleanup,
  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. 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('onEffectCleanup', async () => {
  352. const count = ref(0)
  353. const cleanupEffect = vi.fn()
  354. const cleanupWatch = vi.fn()
  355. const stopEffect = watchEffect(() => {
  356. onEffectCleanup(cleanupEffect)
  357. count.value
  358. })
  359. const stopWatch = watch(count, () => {
  360. onEffectCleanup(cleanupWatch)
  361. })
  362. count.value++
  363. await nextTick()
  364. expect(cleanupEffect).toHaveBeenCalledTimes(1)
  365. expect(cleanupWatch).toHaveBeenCalledTimes(0)
  366. count.value++
  367. await nextTick()
  368. expect(cleanupEffect).toHaveBeenCalledTimes(2)
  369. expect(cleanupWatch).toHaveBeenCalledTimes(1)
  370. stopEffect()
  371. expect(cleanupEffect).toHaveBeenCalledTimes(3)
  372. stopWatch()
  373. expect(cleanupWatch).toHaveBeenCalledTimes(2)
  374. })
  375. it('flush timing: pre (default)', async () => {
  376. const count = ref(0)
  377. const count2 = ref(0)
  378. let callCount = 0
  379. let result1
  380. let result2
  381. const assertion = vi.fn((count, count2Value) => {
  382. callCount++
  383. // on mount, the watcher callback should be called before DOM render
  384. // on update, should be called before the count is updated
  385. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  386. result1 = serializeInner(root) === expectedDOM
  387. // in a pre-flush callback, all state should have been updated
  388. const expectedState = callCount - 1
  389. result2 = count === expectedState && count2Value === expectedState
  390. })
  391. const Comp = {
  392. setup() {
  393. watchEffect(() => {
  394. assertion(count.value, count2.value)
  395. })
  396. return () => count.value
  397. },
  398. }
  399. const root = nodeOps.createElement('div')
  400. render(h(Comp), root)
  401. expect(assertion).toHaveBeenCalledTimes(1)
  402. expect(result1).toBe(true)
  403. expect(result2).toBe(true)
  404. count.value++
  405. count2.value++
  406. await nextTick()
  407. // two mutations should result in 1 callback execution
  408. expect(assertion).toHaveBeenCalledTimes(2)
  409. expect(result1).toBe(true)
  410. expect(result2).toBe(true)
  411. })
  412. it('flush timing: post', async () => {
  413. const count = ref(0)
  414. let result
  415. const assertion = vi.fn(count => {
  416. result = serializeInner(root) === `${count}`
  417. })
  418. const Comp = {
  419. setup() {
  420. watchEffect(
  421. () => {
  422. assertion(count.value)
  423. },
  424. { flush: 'post' },
  425. )
  426. return () => count.value
  427. },
  428. }
  429. const root = nodeOps.createElement('div')
  430. render(h(Comp), root)
  431. expect(assertion).toHaveBeenCalledTimes(1)
  432. expect(result).toBe(true)
  433. count.value++
  434. await nextTick()
  435. expect(assertion).toHaveBeenCalledTimes(2)
  436. expect(result).toBe(true)
  437. })
  438. it('watchPostEffect', async () => {
  439. const count = ref(0)
  440. let result
  441. const assertion = vi.fn(count => {
  442. result = serializeInner(root) === `${count}`
  443. })
  444. const Comp = {
  445. setup() {
  446. watchPostEffect(() => {
  447. assertion(count.value)
  448. })
  449. return () => count.value
  450. },
  451. }
  452. const root = nodeOps.createElement('div')
  453. render(h(Comp), root)
  454. expect(assertion).toHaveBeenCalledTimes(1)
  455. expect(result).toBe(true)
  456. count.value++
  457. await nextTick()
  458. expect(assertion).toHaveBeenCalledTimes(2)
  459. expect(result).toBe(true)
  460. })
  461. it('flush timing: sync', async () => {
  462. const count = ref(0)
  463. const count2 = ref(0)
  464. let callCount = 0
  465. let result1
  466. let result2
  467. const assertion = vi.fn(count => {
  468. callCount++
  469. // on mount, the watcher callback should be called before DOM render
  470. // on update, should be called before the count is updated
  471. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  472. result1 = serializeInner(root) === expectedDOM
  473. // in a sync callback, state mutation on the next line should not have
  474. // executed yet on the 2nd call, but will be on the 3rd call.
  475. const expectedState = callCount < 3 ? 0 : 1
  476. result2 = count2.value === expectedState
  477. })
  478. const Comp = {
  479. setup() {
  480. watchEffect(
  481. () => {
  482. assertion(count.value)
  483. },
  484. {
  485. flush: 'sync',
  486. },
  487. )
  488. return () => count.value
  489. },
  490. }
  491. const root = nodeOps.createElement('div')
  492. render(h(Comp), root)
  493. expect(assertion).toHaveBeenCalledTimes(1)
  494. expect(result1).toBe(true)
  495. expect(result2).toBe(true)
  496. count.value++
  497. count2.value++
  498. await nextTick()
  499. expect(assertion).toHaveBeenCalledTimes(3)
  500. expect(result1).toBe(true)
  501. expect(result2).toBe(true)
  502. })
  503. it('watchSyncEffect', async () => {
  504. const count = ref(0)
  505. const count2 = ref(0)
  506. let callCount = 0
  507. let result1
  508. let result2
  509. const assertion = vi.fn(count => {
  510. callCount++
  511. // on mount, the watcher callback should be called before DOM render
  512. // on update, should be called before the count is updated
  513. const expectedDOM = callCount === 1 ? `` : `${count - 1}`
  514. result1 = serializeInner(root) === expectedDOM
  515. // in a sync callback, state mutation on the next line should not have
  516. // executed yet on the 2nd call, but will be on the 3rd call.
  517. const expectedState = callCount < 3 ? 0 : 1
  518. result2 = count2.value === expectedState
  519. })
  520. const Comp = {
  521. setup() {
  522. watchSyncEffect(() => {
  523. assertion(count.value)
  524. })
  525. return () => count.value
  526. },
  527. }
  528. const root = nodeOps.createElement('div')
  529. render(h(Comp), root)
  530. expect(assertion).toHaveBeenCalledTimes(1)
  531. expect(result1).toBe(true)
  532. expect(result2).toBe(true)
  533. count.value++
  534. count2.value++
  535. await nextTick()
  536. expect(assertion).toHaveBeenCalledTimes(3)
  537. expect(result1).toBe(true)
  538. expect(result2).toBe(true)
  539. })
  540. it('should not fire on component unmount w/ flush: post', async () => {
  541. const toggle = ref(true)
  542. const cb = vi.fn()
  543. const Comp = {
  544. setup() {
  545. watch(toggle, cb, { flush: 'post' })
  546. },
  547. render() {},
  548. }
  549. const App = {
  550. render() {
  551. return toggle.value ? h(Comp) : null
  552. },
  553. }
  554. render(h(App), nodeOps.createElement('div'))
  555. expect(cb).not.toHaveBeenCalled()
  556. toggle.value = false
  557. await nextTick()
  558. expect(cb).not.toHaveBeenCalled()
  559. })
  560. // #2291
  561. it('should not fire on component unmount w/ flush: pre', async () => {
  562. const toggle = ref(true)
  563. const cb = vi.fn()
  564. const Comp = {
  565. setup() {
  566. watch(toggle, cb, { flush: 'pre' })
  567. },
  568. render() {},
  569. }
  570. const App = {
  571. render() {
  572. return toggle.value ? h(Comp) : null
  573. },
  574. }
  575. render(h(App), nodeOps.createElement('div'))
  576. expect(cb).not.toHaveBeenCalled()
  577. toggle.value = false
  578. await nextTick()
  579. expect(cb).not.toHaveBeenCalled()
  580. })
  581. // #7030
  582. it('should not fire on child component unmount w/ flush: pre', async () => {
  583. const visible = ref(true)
  584. const cb = vi.fn()
  585. const Parent = defineComponent({
  586. props: ['visible'],
  587. render() {
  588. return visible.value ? h(Comp) : null
  589. },
  590. })
  591. const Comp = {
  592. setup() {
  593. watch(visible, cb, { flush: 'pre' })
  594. },
  595. render() {},
  596. }
  597. const App = {
  598. render() {
  599. return h(Parent, {
  600. visible: visible.value,
  601. })
  602. },
  603. }
  604. render(h(App), nodeOps.createElement('div'))
  605. expect(cb).not.toHaveBeenCalled()
  606. visible.value = false
  607. await nextTick()
  608. expect(cb).not.toHaveBeenCalled()
  609. })
  610. // #7030
  611. it('flush: pre watcher in child component should not fire before parent update', async () => {
  612. const b = ref(0)
  613. const calls: string[] = []
  614. const Comp = {
  615. setup() {
  616. watch(
  617. () => b.value,
  618. val => {
  619. calls.push('watcher child')
  620. },
  621. { flush: 'pre' },
  622. )
  623. return () => {
  624. b.value
  625. calls.push('render child')
  626. }
  627. },
  628. }
  629. const Parent = {
  630. props: ['a'],
  631. setup() {
  632. watch(
  633. () => b.value,
  634. val => {
  635. calls.push('watcher parent')
  636. },
  637. { flush: 'pre' },
  638. )
  639. return () => {
  640. b.value
  641. calls.push('render parent')
  642. return h(Comp)
  643. }
  644. },
  645. }
  646. const App = {
  647. render() {
  648. return h(Parent, {
  649. a: b.value,
  650. })
  651. },
  652. }
  653. render(h(App), nodeOps.createElement('div'))
  654. expect(calls).toEqual(['render parent', 'render child'])
  655. b.value++
  656. await nextTick()
  657. expect(calls).toEqual([
  658. 'render parent',
  659. 'render child',
  660. 'watcher parent',
  661. 'render parent',
  662. 'watcher child',
  663. 'render child',
  664. ])
  665. })
  666. // #1763
  667. it('flush: pre watcher watching props should fire before child update', async () => {
  668. const a = ref(0)
  669. const b = ref(0)
  670. const c = ref(0)
  671. const calls: string[] = []
  672. const Comp = {
  673. props: ['a', 'b'],
  674. setup(props: any) {
  675. watch(
  676. () => props.a + props.b,
  677. () => {
  678. calls.push('watcher 1')
  679. c.value++
  680. },
  681. { flush: 'pre' },
  682. )
  683. // #1777 chained pre-watcher
  684. watch(
  685. c,
  686. () => {
  687. calls.push('watcher 2')
  688. },
  689. { flush: 'pre' },
  690. )
  691. return () => {
  692. c.value
  693. calls.push('render')
  694. }
  695. },
  696. }
  697. const App = {
  698. render() {
  699. return h(Comp, { a: a.value, b: b.value })
  700. },
  701. }
  702. render(h(App), nodeOps.createElement('div'))
  703. expect(calls).toEqual(['render'])
  704. // both props are updated
  705. // should trigger pre-flush watcher first and only once
  706. // then trigger child render
  707. a.value++
  708. b.value++
  709. await nextTick()
  710. expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
  711. })
  712. // #5721
  713. it('flush: pre triggered in component setup should be buffered and called before mounted', () => {
  714. const count = ref(0)
  715. const calls: string[] = []
  716. const App = {
  717. render() {},
  718. setup() {
  719. watch(
  720. count,
  721. () => {
  722. calls.push('watch ' + count.value)
  723. },
  724. { flush: 'pre' },
  725. )
  726. onMounted(() => {
  727. calls.push('mounted')
  728. })
  729. // mutate multiple times
  730. count.value++
  731. count.value++
  732. count.value++
  733. },
  734. }
  735. render(h(App), nodeOps.createElement('div'))
  736. expect(calls).toMatchObject(['watch 3', 'mounted'])
  737. })
  738. // #1852
  739. it('flush: post watcher should fire after template refs updated', async () => {
  740. const toggle = ref(false)
  741. let dom: TestElement | null = null
  742. const App = {
  743. setup() {
  744. const domRef = ref<TestElement | null>(null)
  745. watch(
  746. toggle,
  747. () => {
  748. dom = domRef.value
  749. },
  750. { flush: 'post' },
  751. )
  752. return () => {
  753. return toggle.value ? h('p', { ref: domRef }) : null
  754. }
  755. },
  756. }
  757. render(h(App), nodeOps.createElement('div'))
  758. expect(dom).toBe(null)
  759. toggle.value = true
  760. await nextTick()
  761. expect(dom!.tag).toBe('p')
  762. })
  763. it('deep', async () => {
  764. const state = reactive({
  765. nested: {
  766. count: ref(0),
  767. },
  768. array: [1, 2, 3],
  769. map: new Map([
  770. ['a', 1],
  771. ['b', 2],
  772. ]),
  773. set: new Set([1, 2, 3]),
  774. })
  775. let dummy
  776. watch(
  777. () => state,
  778. state => {
  779. dummy = [
  780. state.nested.count,
  781. state.array[0],
  782. state.map.get('a'),
  783. state.set.has(1),
  784. ]
  785. },
  786. { deep: true },
  787. )
  788. state.nested.count++
  789. await nextTick()
  790. expect(dummy).toEqual([1, 1, 1, true])
  791. // nested array mutation
  792. state.array[0] = 2
  793. await nextTick()
  794. expect(dummy).toEqual([1, 2, 1, true])
  795. // nested map mutation
  796. state.map.set('a', 2)
  797. await nextTick()
  798. expect(dummy).toEqual([1, 2, 2, true])
  799. // nested set mutation
  800. state.set.delete(1)
  801. await nextTick()
  802. expect(dummy).toEqual([1, 2, 2, false])
  803. })
  804. it('watching deep ref', async () => {
  805. const count = ref(0)
  806. const double = computed(() => count.value * 2)
  807. const state = reactive([count, double])
  808. let dummy
  809. watch(
  810. () => state,
  811. state => {
  812. dummy = [state[0].value, state[1].value]
  813. },
  814. { deep: true },
  815. )
  816. count.value++
  817. await nextTick()
  818. expect(dummy).toEqual([1, 2])
  819. })
  820. it('immediate', async () => {
  821. const count = ref(0)
  822. const cb = vi.fn()
  823. watch(count, cb, { immediate: true })
  824. expect(cb).toHaveBeenCalledTimes(1)
  825. count.value++
  826. await nextTick()
  827. expect(cb).toHaveBeenCalledTimes(2)
  828. })
  829. it('immediate: triggers when initial value is null', async () => {
  830. const state = ref(null)
  831. const spy = vi.fn()
  832. watch(() => state.value, spy, { immediate: true })
  833. expect(spy).toHaveBeenCalled()
  834. })
  835. it('immediate: triggers when initial value is undefined', async () => {
  836. const state = ref()
  837. const spy = vi.fn()
  838. watch(() => state.value, spy, { immediate: true })
  839. expect(spy).toHaveBeenCalledWith(undefined, undefined, expect.any(Function))
  840. state.value = 3
  841. await nextTick()
  842. expect(spy).toHaveBeenCalledTimes(2)
  843. // testing if undefined can trigger the watcher
  844. state.value = undefined
  845. await nextTick()
  846. expect(spy).toHaveBeenCalledTimes(3)
  847. // it shouldn't trigger if the same value is set
  848. state.value = undefined
  849. await nextTick()
  850. expect(spy).toHaveBeenCalledTimes(3)
  851. })
  852. it('warn immediate option when using effect', async () => {
  853. const count = ref(0)
  854. let dummy
  855. watchEffect(
  856. () => {
  857. dummy = count.value
  858. },
  859. // @ts-expect-error
  860. { immediate: false },
  861. )
  862. expect(dummy).toBe(0)
  863. expect(`"immediate" option is only respected`).toHaveBeenWarned()
  864. count.value++
  865. await nextTick()
  866. expect(dummy).toBe(1)
  867. })
  868. it('warn and not respect deep option when using effect', async () => {
  869. const arr = ref([1, [2]])
  870. const spy = vi.fn()
  871. watchEffect(
  872. () => {
  873. spy()
  874. return arr
  875. },
  876. // @ts-expect-error
  877. { deep: true },
  878. )
  879. expect(spy).toHaveBeenCalledTimes(1)
  880. ;(arr.value[1] as Array<number>)[0] = 3
  881. await nextTick()
  882. expect(spy).toHaveBeenCalledTimes(1)
  883. expect(`"deep" option is only respected`).toHaveBeenWarned()
  884. })
  885. it('onTrack', async () => {
  886. const events: DebuggerEvent[] = []
  887. let dummy
  888. const onTrack = vi.fn((e: DebuggerEvent) => {
  889. events.push(e)
  890. })
  891. const obj = reactive({ foo: 1, bar: 2 })
  892. watchEffect(
  893. () => {
  894. dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
  895. },
  896. { onTrack },
  897. )
  898. await nextTick()
  899. expect(dummy).toEqual([1, true, ['foo', 'bar']])
  900. expect(onTrack).toHaveBeenCalledTimes(3)
  901. expect(events).toMatchObject([
  902. {
  903. target: obj,
  904. type: TrackOpTypes.GET,
  905. key: 'foo',
  906. },
  907. {
  908. target: obj,
  909. type: TrackOpTypes.HAS,
  910. key: 'bar',
  911. },
  912. {
  913. target: obj,
  914. type: TrackOpTypes.ITERATE,
  915. key: ITERATE_KEY,
  916. },
  917. ])
  918. })
  919. it('onTrigger', async () => {
  920. const events: DebuggerEvent[] = []
  921. let dummy
  922. const onTrigger = vi.fn((e: DebuggerEvent) => {
  923. events.push(e)
  924. })
  925. const obj = reactive<{ foo?: number }>({ foo: 1 })
  926. watchEffect(
  927. () => {
  928. dummy = obj.foo
  929. },
  930. { onTrigger },
  931. )
  932. await nextTick()
  933. expect(dummy).toBe(1)
  934. obj.foo!++
  935. await nextTick()
  936. expect(dummy).toBe(2)
  937. expect(onTrigger).toHaveBeenCalledTimes(1)
  938. expect(events[0]).toMatchObject({
  939. type: TriggerOpTypes.SET,
  940. key: 'foo',
  941. oldValue: 1,
  942. newValue: 2,
  943. })
  944. delete obj.foo
  945. await nextTick()
  946. expect(dummy).toBeUndefined()
  947. expect(onTrigger).toHaveBeenCalledTimes(2)
  948. expect(events[1]).toMatchObject({
  949. type: TriggerOpTypes.DELETE,
  950. key: 'foo',
  951. oldValue: 2,
  952. })
  953. })
  954. it('should work sync', () => {
  955. const v = ref(1)
  956. let calls = 0
  957. watch(
  958. v,
  959. () => {
  960. ++calls
  961. },
  962. {
  963. flush: 'sync',
  964. },
  965. )
  966. expect(calls).toBe(0)
  967. v.value++
  968. expect(calls).toBe(1)
  969. })
  970. test('should force trigger on triggerRef when watching a shallow ref', async () => {
  971. const v = shallowRef({ a: 1 })
  972. let sideEffect = 0
  973. watch(v, obj => {
  974. sideEffect = obj.a
  975. })
  976. v.value = v.value
  977. await nextTick()
  978. // should not trigger
  979. expect(sideEffect).toBe(0)
  980. v.value.a++
  981. await nextTick()
  982. // should not trigger
  983. expect(sideEffect).toBe(0)
  984. triggerRef(v)
  985. await nextTick()
  986. // should trigger now
  987. expect(sideEffect).toBe(2)
  988. })
  989. test('should force trigger on triggerRef when watching multiple sources: shallow ref array', async () => {
  990. const v = shallowRef([] as any)
  991. const spy = vi.fn()
  992. watch([v], () => {
  993. spy()
  994. })
  995. v.value.push(1)
  996. triggerRef(v)
  997. await nextTick()
  998. // should trigger now
  999. expect(spy).toHaveBeenCalledTimes(1)
  1000. })
  1001. test('should force trigger on triggerRef with toRef from reactive', async () => {
  1002. const foo = reactive({ bar: 1 })
  1003. const bar = toRef(foo, 'bar')
  1004. const spy = vi.fn()
  1005. watchEffect(() => {
  1006. bar.value
  1007. spy()
  1008. })
  1009. expect(spy).toHaveBeenCalledTimes(1)
  1010. triggerRef(bar)
  1011. await nextTick()
  1012. // should trigger now
  1013. expect(spy).toHaveBeenCalledTimes(2)
  1014. })
  1015. // #2125
  1016. test('watchEffect should not recursively trigger itself', async () => {
  1017. const spy = vi.fn()
  1018. const price = ref(10)
  1019. const history = ref<number[]>([])
  1020. watchEffect(() => {
  1021. history.value.push(price.value)
  1022. spy()
  1023. })
  1024. await nextTick()
  1025. expect(spy).toHaveBeenCalledTimes(1)
  1026. })
  1027. // #2231
  1028. test('computed refs should not trigger watch if value has no change', async () => {
  1029. const spy = vi.fn()
  1030. const source = ref(0)
  1031. const price = computed(() => source.value === 0)
  1032. watch(price, spy)
  1033. source.value++
  1034. await nextTick()
  1035. source.value++
  1036. await nextTick()
  1037. expect(spy).toHaveBeenCalledTimes(1)
  1038. })
  1039. // https://github.com/vuejs/core/issues/2381
  1040. test('$watch should always register its effects with its own instance', async () => {
  1041. let instance: ComponentInternalInstance | null
  1042. let _show: Ref<boolean>
  1043. const Child = defineComponent({
  1044. render: () => h('div'),
  1045. mounted() {
  1046. instance = getCurrentInstance()
  1047. },
  1048. unmounted() {},
  1049. })
  1050. const Comp = defineComponent({
  1051. setup() {
  1052. const comp = ref<ComponentPublicInstance | undefined>()
  1053. const show = ref(true)
  1054. _show = show
  1055. return { comp, show }
  1056. },
  1057. render() {
  1058. return this.show
  1059. ? h(Child, {
  1060. ref: vm => void (this.comp = vm as ComponentPublicInstance),
  1061. })
  1062. : null
  1063. },
  1064. mounted() {
  1065. // this call runs while Comp is currentInstance, but
  1066. // the effect for this `$watch` should nonetheless be registered with Child
  1067. this.comp!.$watch(
  1068. () => this.show,
  1069. () => void 0,
  1070. )
  1071. },
  1072. })
  1073. render(h(Comp), nodeOps.createElement('div'))
  1074. expect(instance!).toBeDefined()
  1075. expect(instance!.scope.effects).toBeInstanceOf(Array)
  1076. // includes the component's own render effect AND the watcher effect
  1077. expect(instance!.scope.effects.length).toBe(2)
  1078. _show!.value = false
  1079. await nextTick()
  1080. await nextTick()
  1081. expect(instance!.scope.effects[0].active).toBe(false)
  1082. })
  1083. test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
  1084. let instance: any
  1085. const source = vi.fn()
  1086. const Comp = defineComponent({
  1087. render() {},
  1088. created(this: any) {
  1089. instance = this
  1090. this.$watch(source, function () {})
  1091. },
  1092. })
  1093. const root = nodeOps.createElement('div')
  1094. createApp(Comp).mount(root)
  1095. expect(instance).toBeDefined()
  1096. expect(source.mock.calls.some(args => args.includes(instance)))
  1097. })
  1098. test('should not leak `this.proxy` to setup()', () => {
  1099. const source = vi.fn()
  1100. const Comp = defineComponent({
  1101. render() {},
  1102. setup() {
  1103. watch(source, () => {})
  1104. },
  1105. })
  1106. const root = nodeOps.createElement('div')
  1107. createApp(Comp).mount(root)
  1108. // should not have any arguments
  1109. expect(source.mock.calls[0]).toMatchObject([])
  1110. })
  1111. // #2728
  1112. test('pre watcher callbacks should not track dependencies', async () => {
  1113. const a = ref(0)
  1114. const b = ref(0)
  1115. const updated = vi.fn()
  1116. const Child = defineComponent({
  1117. props: ['a'],
  1118. updated,
  1119. watch: {
  1120. a() {
  1121. b.value
  1122. },
  1123. },
  1124. render() {
  1125. return h('div', this.a)
  1126. },
  1127. })
  1128. const Parent = defineComponent({
  1129. render() {
  1130. return h(Child, { a: a.value })
  1131. },
  1132. })
  1133. const root = nodeOps.createElement('div')
  1134. createApp(Parent).mount(root)
  1135. a.value++
  1136. await nextTick()
  1137. expect(updated).toHaveBeenCalledTimes(1)
  1138. b.value++
  1139. await nextTick()
  1140. // should not track b as dependency of Child
  1141. expect(updated).toHaveBeenCalledTimes(1)
  1142. })
  1143. test('watching keypath', async () => {
  1144. const spy = vi.fn()
  1145. const Comp = defineComponent({
  1146. render() {},
  1147. data() {
  1148. return {
  1149. a: {
  1150. b: 1,
  1151. },
  1152. }
  1153. },
  1154. watch: {
  1155. 'a.b': spy,
  1156. },
  1157. created(this: any) {
  1158. this.$watch('a.b', spy)
  1159. },
  1160. mounted(this: any) {
  1161. this.a.b++
  1162. },
  1163. })
  1164. const root = nodeOps.createElement('div')
  1165. createApp(Comp).mount(root)
  1166. await nextTick()
  1167. expect(spy).toHaveBeenCalledTimes(2)
  1168. })
  1169. it('watching sources: ref<any[]>', async () => {
  1170. const foo = ref([1])
  1171. const spy = vi.fn()
  1172. watch(foo, () => {
  1173. spy()
  1174. })
  1175. foo.value = foo.value.slice()
  1176. await nextTick()
  1177. expect(spy).toBeCalledTimes(1)
  1178. })
  1179. it('watching multiple sources: computed', async () => {
  1180. let count = 0
  1181. const value = ref('1')
  1182. const plus = computed(() => !!value.value)
  1183. watch([plus], () => {
  1184. count++
  1185. })
  1186. value.value = '2'
  1187. await nextTick()
  1188. expect(plus.value).toBe(true)
  1189. expect(count).toBe(0)
  1190. })
  1191. // #4158
  1192. test('watch should not register in owner component if created inside detached scope', () => {
  1193. let instance: ComponentInternalInstance
  1194. const Comp = {
  1195. setup() {
  1196. instance = getCurrentInstance()!
  1197. effectScope(true).run(() => {
  1198. watch(
  1199. () => 1,
  1200. () => {},
  1201. )
  1202. })
  1203. return () => ''
  1204. },
  1205. }
  1206. const root = nodeOps.createElement('div')
  1207. createApp(Comp).mount(root)
  1208. // should not record watcher in detached scope and only the instance's
  1209. // own update effect
  1210. expect(instance!.scope.effects.length).toBe(1)
  1211. })
  1212. test('watchEffect should keep running if created in a detached scope', async () => {
  1213. const trigger = ref(0)
  1214. let countWE = 0
  1215. let countW = 0
  1216. const Comp = {
  1217. setup() {
  1218. effectScope(true).run(() => {
  1219. watchEffect(() => {
  1220. trigger.value
  1221. countWE++
  1222. })
  1223. watch(trigger, () => countW++)
  1224. })
  1225. return () => ''
  1226. },
  1227. }
  1228. const root = nodeOps.createElement('div')
  1229. render(h(Comp), root)
  1230. // only watchEffect as ran so far
  1231. expect(countWE).toBe(1)
  1232. expect(countW).toBe(0)
  1233. trigger.value++
  1234. await nextTick()
  1235. // both watchers run while component is mounted
  1236. expect(countWE).toBe(2)
  1237. expect(countW).toBe(1)
  1238. render(null, root) // unmount
  1239. await nextTick()
  1240. trigger.value++
  1241. await nextTick()
  1242. // both watchers run again event though component has been unmounted
  1243. expect(countWE).toBe(3)
  1244. expect(countW).toBe(2)
  1245. })
  1246. const options = [
  1247. { name: 'only trigger once watch' },
  1248. {
  1249. deep: true,
  1250. name: 'only trigger once watch with deep',
  1251. },
  1252. {
  1253. flush: 'sync',
  1254. name: 'only trigger once watch with flush: sync',
  1255. },
  1256. {
  1257. flush: 'pre',
  1258. name: 'only trigger once watch with flush: pre',
  1259. },
  1260. {
  1261. immediate: true,
  1262. name: 'only trigger once watch with immediate',
  1263. },
  1264. ] as const
  1265. test.each(options)('$name', async option => {
  1266. const count = ref(0)
  1267. const cb = vi.fn()
  1268. watch(count, cb, { once: true, ...option })
  1269. count.value++
  1270. await nextTick()
  1271. expect(count.value).toBe(1)
  1272. expect(cb).toHaveBeenCalledTimes(1)
  1273. count.value++
  1274. await nextTick()
  1275. expect(count.value).toBe(2)
  1276. expect(cb).toHaveBeenCalledTimes(1)
  1277. })
  1278. // #5151
  1279. test('OnCleanup also needs to be cleaned,', async () => {
  1280. const spy1 = vi.fn()
  1281. const spy2 = vi.fn()
  1282. const num = ref(0)
  1283. watch(num, (value, oldValue, onCleanup) => {
  1284. if (value > 1) {
  1285. return
  1286. }
  1287. spy1()
  1288. onCleanup(() => {
  1289. // OnCleanup also needs to be cleaned
  1290. spy2()
  1291. })
  1292. })
  1293. num.value++
  1294. await nextTick()
  1295. expect(spy1).toHaveBeenCalledTimes(1)
  1296. expect(spy2).toHaveBeenCalledTimes(0)
  1297. num.value++
  1298. await nextTick()
  1299. expect(spy1).toHaveBeenCalledTimes(1)
  1300. expect(spy2).toHaveBeenCalledTimes(1)
  1301. num.value++
  1302. await nextTick()
  1303. // would not be calld when value>1
  1304. expect(spy1).toHaveBeenCalledTimes(1)
  1305. expect(spy2).toHaveBeenCalledTimes(1)
  1306. })
  1307. test("effect should be removed from scope's effects after it is stopped", () => {
  1308. const num = ref(0)
  1309. let unwatch: () => void
  1310. let instance: ComponentInternalInstance
  1311. const Comp = {
  1312. setup() {
  1313. instance = getCurrentInstance()!
  1314. unwatch = watch(num, () => {
  1315. console.log(num.value)
  1316. })
  1317. return () => null
  1318. },
  1319. }
  1320. const root = nodeOps.createElement('div')
  1321. createApp(Comp).mount(root)
  1322. expect(instance!.scope.effects.length).toBe(2)
  1323. unwatch!()
  1324. expect(instance!.scope.effects.length).toBe(1)
  1325. const scope = effectScope()
  1326. scope.run(() => {
  1327. unwatch = watch(num, () => {
  1328. console.log(num.value)
  1329. })
  1330. })
  1331. expect(scope.effects.length).toBe(1)
  1332. unwatch!()
  1333. expect(scope.effects.length).toBe(0)
  1334. })
  1335. })