effect.spec.ts 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. import {
  2. computed,
  3. h,
  4. nextTick,
  5. nodeOps,
  6. ref,
  7. render,
  8. serializeInner,
  9. } from '@vue/runtime-test'
  10. import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
  11. import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
  12. import {
  13. type DebuggerEvent,
  14. type ReactiveEffectRunner,
  15. TrackOpTypes,
  16. TriggerOpTypes,
  17. effect,
  18. markRaw,
  19. reactive,
  20. readonly,
  21. shallowReactive,
  22. stop,
  23. toRaw,
  24. } from '../src/index'
  25. import { type ReactiveNode, endBatch, startBatch } from '../src/system'
  26. describe('reactivity/effect', () => {
  27. it('should run the passed function once (wrapped by a effect)', () => {
  28. const fnSpy = vi.fn(() => {})
  29. effect(fnSpy)
  30. expect(fnSpy).toHaveBeenCalledTimes(1)
  31. })
  32. it('should observe basic properties', () => {
  33. let dummy
  34. const counter = reactive({ num: 0 })
  35. effect(() => (dummy = counter.num))
  36. expect(dummy).toBe(0)
  37. counter.num = 7
  38. expect(dummy).toBe(7)
  39. })
  40. it('should observe multiple properties', () => {
  41. let dummy
  42. const counter = reactive({ num1: 0, num2: 0 })
  43. effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))
  44. expect(dummy).toBe(0)
  45. counter.num1 = counter.num2 = 7
  46. expect(dummy).toBe(21)
  47. })
  48. it('should handle multiple effects', () => {
  49. let dummy1, dummy2
  50. const counter = reactive({ num: 0 })
  51. effect(() => (dummy1 = counter.num))
  52. effect(() => (dummy2 = counter.num))
  53. expect(dummy1).toBe(0)
  54. expect(dummy2).toBe(0)
  55. counter.num++
  56. expect(dummy1).toBe(1)
  57. expect(dummy2).toBe(1)
  58. })
  59. it('should observe nested properties', () => {
  60. let dummy
  61. const counter = reactive({ nested: { num: 0 } })
  62. effect(() => (dummy = counter.nested.num))
  63. expect(dummy).toBe(0)
  64. counter.nested.num = 8
  65. expect(dummy).toBe(8)
  66. })
  67. it('should observe delete operations', () => {
  68. let dummy
  69. const obj = reactive<{
  70. prop?: string
  71. }>({ prop: 'value' })
  72. effect(() => (dummy = obj.prop))
  73. expect(dummy).toBe('value')
  74. delete obj.prop
  75. expect(dummy).toBe(undefined)
  76. })
  77. it('should observe has operations', () => {
  78. let dummy
  79. const obj = reactive<{ prop?: string | number }>({ prop: 'value' })
  80. effect(() => (dummy = 'prop' in obj))
  81. expect(dummy).toBe(true)
  82. delete obj.prop
  83. expect(dummy).toBe(false)
  84. obj.prop = 12
  85. expect(dummy).toBe(true)
  86. })
  87. it('should observe properties on the prototype chain', () => {
  88. let dummy
  89. const counter = reactive<{ num?: number }>({ num: 0 })
  90. const parentCounter = reactive({ num: 2 })
  91. Object.setPrototypeOf(counter, parentCounter)
  92. effect(() => (dummy = counter.num))
  93. expect(dummy).toBe(0)
  94. delete counter.num
  95. expect(dummy).toBe(2)
  96. parentCounter.num = 4
  97. expect(dummy).toBe(4)
  98. counter.num = 3
  99. expect(dummy).toBe(3)
  100. })
  101. it('should observe has operations on the prototype chain', () => {
  102. let dummy
  103. const counter = reactive<{ num?: number }>({ num: 0 })
  104. const parentCounter = reactive<{ num?: number }>({ num: 2 })
  105. Object.setPrototypeOf(counter, parentCounter)
  106. effect(() => (dummy = 'num' in counter))
  107. expect(dummy).toBe(true)
  108. delete counter.num
  109. expect(dummy).toBe(true)
  110. delete parentCounter.num
  111. expect(dummy).toBe(false)
  112. counter.num = 3
  113. expect(dummy).toBe(true)
  114. })
  115. it('should observe inherited property accessors', () => {
  116. let dummy, parentDummy, hiddenValue: any
  117. const obj = reactive<{ prop?: number }>({})
  118. const parent = reactive({
  119. set prop(value) {
  120. hiddenValue = value
  121. },
  122. get prop() {
  123. return hiddenValue
  124. },
  125. })
  126. Object.setPrototypeOf(obj, parent)
  127. effect(() => (dummy = obj.prop))
  128. effect(() => (parentDummy = parent.prop))
  129. expect(dummy).toBe(undefined)
  130. expect(parentDummy).toBe(undefined)
  131. obj.prop = 4
  132. expect(dummy).toBe(4)
  133. // this doesn't work, should it?
  134. // expect(parentDummy).toBe(4)
  135. parent.prop = 2
  136. expect(dummy).toBe(2)
  137. expect(parentDummy).toBe(2)
  138. })
  139. it('should observe function call chains', () => {
  140. let dummy
  141. const counter = reactive({ num: 0 })
  142. effect(() => (dummy = getNum()))
  143. function getNum() {
  144. return counter.num
  145. }
  146. expect(dummy).toBe(0)
  147. counter.num = 2
  148. expect(dummy).toBe(2)
  149. })
  150. it('should observe iteration', () => {
  151. let dummy
  152. const list = reactive(['Hello'])
  153. effect(() => (dummy = list.join(' ')))
  154. expect(dummy).toBe('Hello')
  155. list.push('World!')
  156. expect(dummy).toBe('Hello World!')
  157. list.shift()
  158. expect(dummy).toBe('World!')
  159. })
  160. it('should observe implicit array length changes', () => {
  161. let dummy
  162. const list = reactive(['Hello'])
  163. effect(() => (dummy = list.join(' ')))
  164. expect(dummy).toBe('Hello')
  165. list[1] = 'World!'
  166. expect(dummy).toBe('Hello World!')
  167. list[3] = 'Hello!'
  168. expect(dummy).toBe('Hello World! Hello!')
  169. })
  170. it('should observe sparse array mutations', () => {
  171. let dummy
  172. const list = reactive<string[]>([])
  173. list[1] = 'World!'
  174. effect(() => (dummy = list.join(' ')))
  175. expect(dummy).toBe(' World!')
  176. list[0] = 'Hello'
  177. expect(dummy).toBe('Hello World!')
  178. list.pop()
  179. expect(dummy).toBe('Hello')
  180. })
  181. it('should observe enumeration', () => {
  182. let dummy = 0
  183. const numbers = reactive<Record<string, number>>({ num1: 3 })
  184. effect(() => {
  185. dummy = 0
  186. for (let key in numbers) {
  187. dummy += numbers[key]
  188. }
  189. })
  190. expect(dummy).toBe(3)
  191. numbers.num2 = 4
  192. expect(dummy).toBe(7)
  193. delete numbers.num1
  194. expect(dummy).toBe(4)
  195. })
  196. it('should observe symbol keyed properties', () => {
  197. const key = Symbol('symbol keyed prop')
  198. let dummy, hasDummy
  199. const obj = reactive<{ [key]?: string }>({ [key]: 'value' })
  200. effect(() => (dummy = obj[key]))
  201. effect(() => (hasDummy = key in obj))
  202. expect(dummy).toBe('value')
  203. expect(hasDummy).toBe(true)
  204. obj[key] = 'newValue'
  205. expect(dummy).toBe('newValue')
  206. delete obj[key]
  207. expect(dummy).toBe(undefined)
  208. expect(hasDummy).toBe(false)
  209. })
  210. it('should not observe well-known symbol keyed properties', () => {
  211. const key = Symbol.isConcatSpreadable
  212. let dummy
  213. const array: any = reactive([])
  214. effect(() => (dummy = array[key]))
  215. expect(array[key]).toBe(undefined)
  216. expect(dummy).toBe(undefined)
  217. array[key] = true
  218. expect(array[key]).toBe(true)
  219. expect(dummy).toBe(undefined)
  220. })
  221. it('should not observe well-known symbol keyed properties in has operation', () => {
  222. const key = Symbol.isConcatSpreadable
  223. const obj = reactive({
  224. [key]: true,
  225. }) as any
  226. const spy = vi.fn(() => {
  227. key in obj
  228. })
  229. effect(spy)
  230. expect(spy).toHaveBeenCalledTimes(1)
  231. obj[key] = false
  232. expect(spy).toHaveBeenCalledTimes(1)
  233. })
  234. it('should support manipulating an array while observing symbol keyed properties', () => {
  235. const key = Symbol()
  236. let dummy
  237. const array: any = reactive([1, 2, 3])
  238. effect(() => (dummy = array[key]))
  239. expect(dummy).toBe(undefined)
  240. array.pop()
  241. array.shift()
  242. array.splice(0, 1)
  243. expect(dummy).toBe(undefined)
  244. array[key] = 'value'
  245. array.length = 0
  246. expect(dummy).toBe('value')
  247. })
  248. it('should observe function valued properties', () => {
  249. const oldFunc = () => {}
  250. const newFunc = () => {}
  251. let dummy
  252. const obj = reactive({ func: oldFunc })
  253. effect(() => (dummy = obj.func))
  254. expect(dummy).toBe(oldFunc)
  255. obj.func = newFunc
  256. expect(dummy).toBe(newFunc)
  257. })
  258. it('should observe chained getters relying on this', () => {
  259. const obj = reactive({
  260. a: 1,
  261. get b() {
  262. return this.a
  263. },
  264. })
  265. let dummy
  266. effect(() => (dummy = obj.b))
  267. expect(dummy).toBe(1)
  268. obj.a++
  269. expect(dummy).toBe(2)
  270. })
  271. it('should observe methods relying on this', () => {
  272. const obj = reactive({
  273. a: 1,
  274. b() {
  275. return this.a
  276. },
  277. })
  278. let dummy
  279. effect(() => (dummy = obj.b()))
  280. expect(dummy).toBe(1)
  281. obj.a++
  282. expect(dummy).toBe(2)
  283. })
  284. it('should not observe set operations without a value change', () => {
  285. let hasDummy, getDummy
  286. const obj = reactive({ prop: 'value' })
  287. const getSpy = vi.fn(() => (getDummy = obj.prop))
  288. const hasSpy = vi.fn(() => (hasDummy = 'prop' in obj))
  289. effect(getSpy)
  290. effect(hasSpy)
  291. expect(getDummy).toBe('value')
  292. expect(hasDummy).toBe(true)
  293. obj.prop = 'value'
  294. expect(getSpy).toHaveBeenCalledTimes(1)
  295. expect(hasSpy).toHaveBeenCalledTimes(1)
  296. expect(getDummy).toBe('value')
  297. expect(hasDummy).toBe(true)
  298. })
  299. it('should not observe raw mutations', () => {
  300. let dummy
  301. const obj = reactive<{ prop?: string }>({})
  302. effect(() => (dummy = toRaw(obj).prop))
  303. expect(dummy).toBe(undefined)
  304. obj.prop = 'value'
  305. expect(dummy).toBe(undefined)
  306. })
  307. it('should not be triggered by raw mutations', () => {
  308. let dummy
  309. const obj = reactive<{ prop?: string }>({})
  310. effect(() => (dummy = obj.prop))
  311. expect(dummy).toBe(undefined)
  312. toRaw(obj).prop = 'value'
  313. expect(dummy).toBe(undefined)
  314. })
  315. it('should not be triggered by inherited raw setters', () => {
  316. let dummy, parentDummy, hiddenValue: any
  317. const obj = reactive<{ prop?: number }>({})
  318. const parent = reactive({
  319. set prop(value) {
  320. hiddenValue = value
  321. },
  322. get prop() {
  323. return hiddenValue
  324. },
  325. })
  326. Object.setPrototypeOf(obj, parent)
  327. effect(() => (dummy = obj.prop))
  328. effect(() => (parentDummy = parent.prop))
  329. expect(dummy).toBe(undefined)
  330. expect(parentDummy).toBe(undefined)
  331. toRaw(obj).prop = 4
  332. expect(dummy).toBe(undefined)
  333. expect(parentDummy).toBe(undefined)
  334. })
  335. it('should avoid implicit infinite recursive loops with itself', () => {
  336. const counter = reactive({ num: 0 })
  337. const counterSpy = vi.fn(() => counter.num++)
  338. effect(counterSpy)
  339. expect(counter.num).toBe(1)
  340. expect(counterSpy).toHaveBeenCalledTimes(1)
  341. counter.num = 4
  342. expect(counter.num).toBe(5)
  343. expect(counterSpy).toHaveBeenCalledTimes(2)
  344. })
  345. it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => {
  346. ;(['push', 'unshift'] as const).forEach(key => {
  347. const arr = reactive<number[]>([])
  348. const counterSpy1 = vi.fn(() => (arr[key] as any)(1))
  349. const counterSpy2 = vi.fn(() => (arr[key] as any)(2))
  350. effect(counterSpy1)
  351. effect(counterSpy2)
  352. expect(arr.length).toBe(2)
  353. expect(counterSpy1).toHaveBeenCalledTimes(1)
  354. expect(counterSpy2).toHaveBeenCalledTimes(1)
  355. })
  356. ;(['pop', 'shift'] as const).forEach(key => {
  357. const arr = reactive<number[]>([1, 2, 3, 4])
  358. const counterSpy1 = vi.fn(() => (arr[key] as any)())
  359. const counterSpy2 = vi.fn(() => (arr[key] as any)())
  360. effect(counterSpy1)
  361. effect(counterSpy2)
  362. expect(arr.length).toBe(2)
  363. expect(counterSpy1).toHaveBeenCalledTimes(1)
  364. expect(counterSpy2).toHaveBeenCalledTimes(1)
  365. })
  366. })
  367. it('should allow explicitly recursive raw function loops', () => {
  368. const counter = reactive({ num: 0 })
  369. const numSpy = vi.fn(() => {
  370. counter.num++
  371. if (counter.num < 10) {
  372. numSpy()
  373. }
  374. })
  375. effect(numSpy)
  376. expect(counter.num).toEqual(10)
  377. expect(numSpy).toHaveBeenCalledTimes(10)
  378. })
  379. it('should avoid infinite loops with other effects', () => {
  380. const nums = reactive({ num1: 0, num2: 1 })
  381. const spy1 = vi.fn(() => (nums.num1 = nums.num2))
  382. const spy2 = vi.fn(() => (nums.num2 = nums.num1))
  383. effect(spy1)
  384. effect(spy2)
  385. expect(nums.num1).toBe(1)
  386. expect(nums.num2).toBe(1)
  387. expect(spy1).toHaveBeenCalledTimes(1)
  388. expect(spy2).toHaveBeenCalledTimes(1)
  389. nums.num2 = 4
  390. expect(nums.num1).toBe(4)
  391. expect(nums.num2).toBe(4)
  392. expect(spy1).toHaveBeenCalledTimes(2)
  393. expect(spy2).toHaveBeenCalledTimes(2)
  394. nums.num1 = 10
  395. expect(nums.num1).toBe(10)
  396. expect(nums.num2).toBe(10)
  397. expect(spy1).toHaveBeenCalledTimes(3)
  398. expect(spy2).toHaveBeenCalledTimes(3)
  399. })
  400. it('should return a new reactive version of the function', () => {
  401. function greet() {
  402. return 'Hello World'
  403. }
  404. const effect1 = effect(greet)
  405. const effect2 = effect(greet)
  406. expect(typeof effect1).toBe('function')
  407. expect(typeof effect2).toBe('function')
  408. expect(effect1).not.toBe(greet)
  409. expect(effect1).not.toBe(effect2)
  410. })
  411. it('should discover new branches while running automatically', () => {
  412. let dummy
  413. const obj = reactive({ prop: 'value', run: false })
  414. const conditionalSpy = vi.fn(() => {
  415. dummy = obj.run ? obj.prop : 'other'
  416. })
  417. effect(conditionalSpy)
  418. expect(dummy).toBe('other')
  419. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  420. obj.prop = 'Hi'
  421. expect(dummy).toBe('other')
  422. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  423. obj.run = true
  424. expect(dummy).toBe('Hi')
  425. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  426. obj.prop = 'World'
  427. expect(dummy).toBe('World')
  428. expect(conditionalSpy).toHaveBeenCalledTimes(3)
  429. })
  430. it('should discover new branches when running manually', () => {
  431. let dummy
  432. let run = false
  433. const obj = reactive({ prop: 'value' })
  434. const runner = effect(() => {
  435. dummy = run ? obj.prop : 'other'
  436. })
  437. expect(dummy).toBe('other')
  438. runner()
  439. expect(dummy).toBe('other')
  440. run = true
  441. runner()
  442. expect(dummy).toBe('value')
  443. obj.prop = 'World'
  444. expect(dummy).toBe('World')
  445. })
  446. it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  447. let dummy
  448. const obj = reactive({ prop: 'value', run: true })
  449. const conditionalSpy = vi.fn(() => {
  450. dummy = obj.run ? obj.prop : 'other'
  451. })
  452. effect(conditionalSpy)
  453. expect(dummy).toBe('value')
  454. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  455. obj.run = false
  456. expect(dummy).toBe('other')
  457. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  458. obj.prop = 'value2'
  459. expect(dummy).toBe('other')
  460. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  461. })
  462. it('should handle deep effect recursion using cleanup fallback', () => {
  463. const results = reactive([0])
  464. const effects: { fx: ReactiveEffectRunner; index: number }[] = []
  465. for (let i = 1; i < 40; i++) {
  466. ;(index => {
  467. const fx = effect(() => {
  468. results[index] = results[index - 1] * 2
  469. })
  470. effects.push({ fx, index })
  471. })(i)
  472. }
  473. expect(results[39]).toBe(0)
  474. results[0] = 1
  475. expect(results[39]).toBe(Math.pow(2, 39))
  476. })
  477. it('should register deps independently during effect recursion', () => {
  478. const input = reactive({ a: 1, b: 2, c: 0 })
  479. const output = reactive({ fx1: 0, fx2: 0 })
  480. const fx1Spy = vi.fn(() => {
  481. let result = 0
  482. if (input.c < 2) result += input.a
  483. if (input.c > 1) result += input.b
  484. output.fx1 = result
  485. })
  486. const fx1 = effect(fx1Spy)
  487. const fx2Spy = vi.fn(() => {
  488. let result = 0
  489. if (input.c > 1) result += input.a
  490. if (input.c < 3) result += input.b
  491. output.fx2 = result + output.fx1
  492. })
  493. const fx2 = effect(fx2Spy)
  494. expect(fx1).not.toBeNull()
  495. expect(fx2).not.toBeNull()
  496. expect(output.fx1).toBe(1)
  497. expect(output.fx2).toBe(2 + 1)
  498. expect(fx1Spy).toHaveBeenCalledTimes(1)
  499. expect(fx2Spy).toHaveBeenCalledTimes(1)
  500. fx1Spy.mockClear()
  501. fx2Spy.mockClear()
  502. input.b = 3
  503. expect(output.fx1).toBe(1)
  504. expect(output.fx2).toBe(3 + 1)
  505. expect(fx1Spy).toHaveBeenCalledTimes(0)
  506. expect(fx2Spy).toHaveBeenCalledTimes(1)
  507. fx1Spy.mockClear()
  508. fx2Spy.mockClear()
  509. input.c = 1
  510. expect(output.fx1).toBe(1)
  511. expect(output.fx2).toBe(3 + 1)
  512. expect(fx1Spy).toHaveBeenCalledTimes(1)
  513. expect(fx2Spy).toHaveBeenCalledTimes(1)
  514. fx1Spy.mockClear()
  515. fx2Spy.mockClear()
  516. input.c = 2
  517. expect(output.fx1).toBe(3)
  518. expect(output.fx2).toBe(1 + 3 + 3)
  519. expect(fx1Spy).toHaveBeenCalledTimes(1)
  520. // Invoked due to change of fx1.
  521. expect(fx2Spy).toHaveBeenCalledTimes(1)
  522. fx1Spy.mockClear()
  523. fx2Spy.mockClear()
  524. input.c = 3
  525. expect(output.fx1).toBe(3)
  526. expect(output.fx2).toBe(1 + 3)
  527. expect(fx1Spy).toHaveBeenCalledTimes(1)
  528. expect(fx2Spy).toHaveBeenCalledTimes(1)
  529. fx1Spy.mockClear()
  530. fx2Spy.mockClear()
  531. input.a = 10
  532. expect(output.fx1).toBe(3)
  533. expect(output.fx2).toBe(10 + 3)
  534. expect(fx1Spy).toHaveBeenCalledTimes(0)
  535. expect(fx2Spy).toHaveBeenCalledTimes(1)
  536. })
  537. it('should not double wrap if the passed function is a effect', () => {
  538. const runner = effect(() => {})
  539. const otherRunner = effect(runner)
  540. expect(runner).not.toBe(otherRunner)
  541. expect(runner.effect.fn).toBe(otherRunner.effect.fn)
  542. })
  543. it('should wrap if the passed function is a fake effect', () => {
  544. const fakeRunner = () => {}
  545. fakeRunner.effect = {}
  546. const runner = effect(fakeRunner)
  547. expect(fakeRunner).not.toBe(runner)
  548. expect(runner.effect.fn).toBe(fakeRunner)
  549. })
  550. it('should not run multiple times for a single mutation', () => {
  551. let dummy
  552. const obj = reactive<Record<string, number>>({})
  553. const fnSpy = vi.fn(() => {
  554. for (const key in obj) {
  555. dummy = obj[key]
  556. }
  557. dummy = obj.prop
  558. })
  559. effect(fnSpy)
  560. expect(fnSpy).toHaveBeenCalledTimes(1)
  561. obj.prop = 16
  562. expect(dummy).toBe(16)
  563. expect(fnSpy).toHaveBeenCalledTimes(2)
  564. })
  565. it('should allow nested effects', () => {
  566. const nums = reactive({ num1: 0, num2: 1, num3: 2 })
  567. const dummy: any = {}
  568. const childSpy = vi.fn(() => (dummy.num1 = nums.num1))
  569. const childeffect = effect(childSpy)
  570. const parentSpy = vi.fn(() => {
  571. dummy.num2 = nums.num2
  572. childeffect()
  573. dummy.num3 = nums.num3
  574. })
  575. effect(parentSpy)
  576. expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 })
  577. expect(parentSpy).toHaveBeenCalledTimes(1)
  578. expect(childSpy).toHaveBeenCalledTimes(2)
  579. // this should only call the childeffect
  580. nums.num1 = 4
  581. expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 })
  582. expect(parentSpy).toHaveBeenCalledTimes(1)
  583. expect(childSpy).toHaveBeenCalledTimes(3)
  584. // this calls the parenteffect, which calls the childeffect once
  585. nums.num2 = 10
  586. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 })
  587. expect(parentSpy).toHaveBeenCalledTimes(2)
  588. expect(childSpy).toHaveBeenCalledTimes(4)
  589. // this calls the parenteffect, which calls the childeffect once
  590. nums.num3 = 7
  591. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 })
  592. expect(parentSpy).toHaveBeenCalledTimes(3)
  593. expect(childSpy).toHaveBeenCalledTimes(5)
  594. })
  595. it('should observe json methods', () => {
  596. let dummy = <Record<string, number>>{}
  597. const obj = reactive<Record<string, number>>({})
  598. effect(() => {
  599. dummy = JSON.parse(JSON.stringify(obj))
  600. })
  601. obj.a = 1
  602. expect(dummy.a).toBe(1)
  603. })
  604. it('should observe class method invocations', () => {
  605. class Model {
  606. count: number
  607. constructor() {
  608. this.count = 0
  609. }
  610. inc() {
  611. this.count++
  612. }
  613. }
  614. const model = reactive(new Model())
  615. let dummy
  616. effect(() => {
  617. dummy = model.count
  618. })
  619. expect(dummy).toBe(0)
  620. model.inc()
  621. expect(dummy).toBe(1)
  622. })
  623. it('scheduler', () => {
  624. let dummy
  625. let run: any
  626. const scheduler = vi.fn(() => {
  627. run = runner
  628. })
  629. const obj = reactive({ foo: 1 })
  630. const runner = effect(
  631. () => {
  632. dummy = obj.foo
  633. },
  634. { scheduler },
  635. )
  636. expect(scheduler).not.toHaveBeenCalled()
  637. expect(dummy).toBe(1)
  638. // should be called on first trigger
  639. obj.foo++
  640. expect(scheduler).toHaveBeenCalledTimes(1)
  641. // should not run yet
  642. expect(dummy).toBe(1)
  643. // manually run
  644. run()
  645. // should have run
  646. expect(dummy).toBe(2)
  647. })
  648. it('events: onTrack', () => {
  649. let events: DebuggerEvent[] = []
  650. let dummy
  651. const onTrack = vi.fn((e: DebuggerEvent) => {
  652. events.push(e)
  653. })
  654. const obj = reactive({ foo: 1, bar: 2 })
  655. const runner = effect(
  656. () => {
  657. dummy = obj.foo
  658. dummy = 'bar' in obj
  659. dummy = Object.keys(obj)
  660. },
  661. { onTrack },
  662. )
  663. expect(dummy).toEqual(['foo', 'bar'])
  664. expect(onTrack).toHaveBeenCalledTimes(3)
  665. expect(events).toEqual([
  666. {
  667. effect: runner.effect,
  668. target: toRaw(obj),
  669. type: TrackOpTypes.GET,
  670. key: 'foo',
  671. },
  672. {
  673. effect: runner.effect,
  674. target: toRaw(obj),
  675. type: TrackOpTypes.HAS,
  676. key: 'bar',
  677. },
  678. {
  679. effect: runner.effect,
  680. target: toRaw(obj),
  681. type: TrackOpTypes.ITERATE,
  682. key: ITERATE_KEY,
  683. },
  684. ])
  685. })
  686. it('debug: the call sequence of onTrack', () => {
  687. const seq: number[] = []
  688. const s = ref(0)
  689. const track1 = () => seq.push(1)
  690. const track2 = () => seq.push(2)
  691. effect(
  692. () => {
  693. s.value
  694. },
  695. {
  696. onTrack: track1,
  697. },
  698. )
  699. effect(
  700. () => {
  701. s.value
  702. },
  703. {
  704. onTrack: track2,
  705. },
  706. )
  707. expect(seq.toString()).toBe('1,2')
  708. })
  709. it('events: onTrigger', () => {
  710. let events: DebuggerEvent[] = []
  711. let dummy
  712. const onTrigger = vi.fn((e: DebuggerEvent) => {
  713. events.push(e)
  714. })
  715. const obj = reactive<{ foo?: number }>({ foo: 1 })
  716. const runner = effect(
  717. () => {
  718. dummy = obj.foo
  719. },
  720. { onTrigger },
  721. )
  722. obj.foo!++
  723. expect(dummy).toBe(2)
  724. expect(onTrigger).toHaveBeenCalledTimes(1)
  725. expect(events[0]).toEqual({
  726. effect: runner.effect,
  727. target: toRaw(obj),
  728. type: TriggerOpTypes.SET,
  729. key: 'foo',
  730. oldValue: 1,
  731. newValue: 2,
  732. })
  733. delete obj.foo
  734. expect(dummy).toBeUndefined()
  735. expect(onTrigger).toHaveBeenCalledTimes(2)
  736. expect(events[1]).toEqual({
  737. effect: runner.effect,
  738. target: toRaw(obj),
  739. type: TriggerOpTypes.DELETE,
  740. key: 'foo',
  741. oldValue: 2,
  742. })
  743. })
  744. it('debug: the call sequence of onTrigger', () => {
  745. const seq: number[] = []
  746. const s = ref(0)
  747. const trigger1 = () => seq.push(1)
  748. const trigger2 = () => seq.push(2)
  749. const trigger3 = () => seq.push(3)
  750. const trigger4 = () => seq.push(4)
  751. effect(
  752. () => {
  753. s.value
  754. },
  755. {
  756. onTrigger: trigger1,
  757. },
  758. )
  759. effect(
  760. () => {
  761. s.value
  762. effect(
  763. () => {
  764. s.value
  765. effect(
  766. () => {
  767. s.value
  768. },
  769. {
  770. onTrigger: trigger4,
  771. },
  772. )
  773. },
  774. {
  775. onTrigger: trigger3,
  776. },
  777. )
  778. },
  779. {
  780. onTrigger: trigger2,
  781. },
  782. )
  783. s.value++
  784. expect(seq.toString()).toBe('1,2,3,4')
  785. })
  786. it('stop', () => {
  787. let dummy
  788. const obj = reactive({ prop: 1 })
  789. const runner = effect(() => {
  790. dummy = obj.prop
  791. })
  792. obj.prop = 2
  793. expect(dummy).toBe(2)
  794. stop(runner)
  795. obj.prop = 3
  796. expect(dummy).toBe(2)
  797. // stopped effect should still be manually callable
  798. runner()
  799. expect(dummy).toBe(3)
  800. })
  801. it('stop with multiple dependencies', () => {
  802. let dummy1, dummy2
  803. const obj1 = reactive({ prop: 1 })
  804. const obj2 = reactive({ prop: 1 })
  805. const runner = effect(() => {
  806. dummy1 = obj1.prop
  807. dummy2 = obj2.prop
  808. })
  809. obj1.prop = 2
  810. expect(dummy1).toBe(2)
  811. obj2.prop = 3
  812. expect(dummy2).toBe(3)
  813. stop(runner)
  814. obj1.prop = 4
  815. obj2.prop = 5
  816. // Check that both dependencies have been cleared
  817. expect(dummy1).toBe(2)
  818. expect(dummy2).toBe(3)
  819. })
  820. it('events: onStop', () => {
  821. const onStop = vi.fn()
  822. const runner = effect(() => {}, {
  823. onStop,
  824. })
  825. stop(runner)
  826. expect(onStop).toHaveBeenCalled()
  827. })
  828. it('stop: a stopped effect is nested in a normal effect', () => {
  829. let dummy
  830. const obj = reactive({ prop: 1 })
  831. const runner = effect(() => {
  832. dummy = obj.prop
  833. })
  834. stop(runner)
  835. obj.prop = 2
  836. expect(dummy).toBe(1)
  837. // observed value in inner stopped effect
  838. // will track outer effect as an dependency
  839. effect(() => {
  840. runner()
  841. })
  842. expect(dummy).toBe(2)
  843. // notify outer effect to run
  844. obj.prop = 3
  845. expect(dummy).toBe(3)
  846. })
  847. it('markRaw', () => {
  848. const obj = reactive({
  849. foo: markRaw({
  850. prop: 0,
  851. }),
  852. })
  853. let dummy
  854. effect(() => {
  855. dummy = obj.foo.prop
  856. })
  857. expect(dummy).toBe(0)
  858. obj.foo.prop++
  859. expect(dummy).toBe(0)
  860. obj.foo = { prop: 1 }
  861. expect(dummy).toBe(1)
  862. })
  863. it('should not be triggered when the value and the old value both are NaN', () => {
  864. const obj = reactive({
  865. foo: NaN,
  866. })
  867. const fnSpy = vi.fn(() => obj.foo)
  868. effect(fnSpy)
  869. obj.foo = NaN
  870. expect(fnSpy).toHaveBeenCalledTimes(1)
  871. })
  872. it('should trigger all effects when array length is set to 0', () => {
  873. const observed: any = reactive([1])
  874. let dummy, record
  875. effect(() => {
  876. dummy = observed.length
  877. })
  878. effect(() => {
  879. record = observed[0]
  880. })
  881. expect(dummy).toBe(1)
  882. expect(record).toBe(1)
  883. observed[1] = 2
  884. expect(observed[1]).toBe(2)
  885. observed.unshift(3)
  886. expect(dummy).toBe(3)
  887. expect(record).toBe(3)
  888. observed.length = 0
  889. expect(dummy).toBe(0)
  890. expect(record).toBeUndefined()
  891. })
  892. it('should not be triggered when set with the same proxy', () => {
  893. const obj = reactive({ foo: 1 })
  894. const observed: any = reactive({ obj })
  895. const fnSpy = vi.fn(() => observed.obj)
  896. effect(fnSpy)
  897. expect(fnSpy).toHaveBeenCalledTimes(1)
  898. observed.obj = obj
  899. expect(fnSpy).toHaveBeenCalledTimes(1)
  900. const obj2 = reactive({ foo: 1 })
  901. const observed2: any = shallowReactive({ obj2 })
  902. const fnSpy2 = vi.fn(() => observed2.obj2)
  903. effect(fnSpy2)
  904. expect(fnSpy2).toHaveBeenCalledTimes(1)
  905. observed2.obj2 = obj2
  906. expect(fnSpy2).toHaveBeenCalledTimes(1)
  907. })
  908. it('should be triggered when set length with string', () => {
  909. let ret1 = 'idle'
  910. let ret2 = 'idle'
  911. const arr1 = reactive(new Array(11).fill(0))
  912. const arr2 = reactive(new Array(11).fill(0))
  913. effect(() => {
  914. ret1 = arr1[10] === undefined ? 'arr[10] is set to empty' : 'idle'
  915. })
  916. effect(() => {
  917. ret2 = arr2[10] === undefined ? 'arr[10] is set to empty' : 'idle'
  918. })
  919. arr1.length = 2
  920. arr2.length = '2' as any
  921. expect(ret1).toBe(ret2)
  922. })
  923. describe('readonly + reactive for Map', () => {
  924. test('should work with readonly(reactive(Map))', () => {
  925. const m = reactive(new Map())
  926. const roM = readonly(m)
  927. const fnSpy = vi.fn(() => roM.get(1))
  928. effect(fnSpy)
  929. expect(fnSpy).toHaveBeenCalledTimes(1)
  930. m.set(1, 1)
  931. expect(fnSpy).toHaveBeenCalledTimes(2)
  932. })
  933. test('should work with observed value as key', () => {
  934. const key = reactive({})
  935. const m = reactive(new Map())
  936. m.set(key, 1)
  937. const roM = readonly(m)
  938. const fnSpy = vi.fn(() => roM.get(key))
  939. effect(fnSpy)
  940. expect(fnSpy).toHaveBeenCalledTimes(1)
  941. m.set(key, 1)
  942. expect(fnSpy).toHaveBeenCalledTimes(1)
  943. m.set(key, 2)
  944. expect(fnSpy).toHaveBeenCalledTimes(2)
  945. })
  946. test('should track hasOwnProperty', () => {
  947. const obj: any = reactive({})
  948. let has = false
  949. const fnSpy = vi.fn()
  950. effect(() => {
  951. fnSpy()
  952. has = obj.hasOwnProperty('foo')
  953. })
  954. expect(fnSpy).toHaveBeenCalledTimes(1)
  955. expect(has).toBe(false)
  956. obj.foo = 1
  957. expect(fnSpy).toHaveBeenCalledTimes(2)
  958. expect(has).toBe(true)
  959. delete obj.foo
  960. expect(fnSpy).toHaveBeenCalledTimes(3)
  961. expect(has).toBe(false)
  962. // should not trigger on unrelated key
  963. obj.bar = 2
  964. expect(fnSpy).toHaveBeenCalledTimes(3)
  965. expect(has).toBe(false)
  966. })
  967. })
  968. it('should be triggered once with batching', () => {
  969. const counter = reactive({ num: 0 })
  970. const counterSpy = vi.fn(() => counter.num)
  971. effect(counterSpy)
  972. counterSpy.mockClear()
  973. startBatch()
  974. counter.num++
  975. counter.num++
  976. endBatch()
  977. expect(counterSpy).toHaveBeenCalledTimes(1)
  978. })
  979. // #10082
  980. it('should set dirtyLevel when effect is allowRecurse and is running', async () => {
  981. const s = ref(0)
  982. const n = computed(() => s.value + 1)
  983. const Child = {
  984. setup() {
  985. s.value++
  986. return () => n.value
  987. },
  988. }
  989. const renderSpy = vi.fn()
  990. const Parent = {
  991. setup() {
  992. return () => {
  993. renderSpy()
  994. return [n.value, h(Child)]
  995. }
  996. },
  997. }
  998. const root = nodeOps.createElement('div')
  999. render(h(Parent), root)
  1000. await nextTick()
  1001. expect(serializeInner(root)).toBe('22')
  1002. expect(renderSpy).toHaveBeenCalledTimes(2)
  1003. })
  1004. it('nested effect should force track in untracked zone', () => {
  1005. const n = ref(0)
  1006. const spy1 = vi.fn()
  1007. const spy2 = vi.fn()
  1008. effect(() => {
  1009. spy1()
  1010. pauseTracking()
  1011. n.value
  1012. effect(() => {
  1013. n.value
  1014. spy2()
  1015. })
  1016. n.value
  1017. resetTracking()
  1018. })
  1019. expect(spy1).toHaveBeenCalledTimes(1)
  1020. expect(spy2).toHaveBeenCalledTimes(1)
  1021. n.value++
  1022. // outer effect should not trigger
  1023. expect(spy1).toHaveBeenCalledTimes(1)
  1024. // inner effect should trigger
  1025. expect(spy2).toHaveBeenCalledTimes(2)
  1026. })
  1027. describe('dep unsubscribe', () => {
  1028. function getSubCount(dep: ReactiveNode | undefined) {
  1029. let count = 0
  1030. let sub = dep!.subs
  1031. while (sub) {
  1032. count++
  1033. sub = sub.nextSub
  1034. }
  1035. return count
  1036. }
  1037. it('should remove the dep when the effect is stopped', () => {
  1038. const obj = reactive({ prop: 1 })
  1039. const runner = effect(() => obj.prop)
  1040. const dep = getDepFromReactive(toRaw(obj), 'prop')
  1041. expect(getSubCount(dep)).toBe(1)
  1042. obj.prop = 2
  1043. expect(getSubCount(dep)).toBe(1)
  1044. stop(runner)
  1045. expect(getSubCount(dep)).toBe(0)
  1046. obj.prop = 3
  1047. runner()
  1048. expect(getSubCount(dep)).toBe(0)
  1049. })
  1050. it('should only remove the dep when the last effect is stopped', () => {
  1051. const obj = reactive({ prop: 1 })
  1052. const runner1 = effect(() => obj.prop)
  1053. const dep = getDepFromReactive(toRaw(obj), 'prop')
  1054. expect(getSubCount(dep)).toBe(1)
  1055. const runner2 = effect(() => obj.prop)
  1056. expect(getSubCount(dep)).toBe(2)
  1057. obj.prop = 2
  1058. expect(getSubCount(dep)).toBe(2)
  1059. stop(runner1)
  1060. expect(getSubCount(dep)).toBe(1)
  1061. obj.prop = 3
  1062. expect(getSubCount(dep)).toBe(1)
  1063. stop(runner2)
  1064. obj.prop = 4
  1065. runner1()
  1066. runner2()
  1067. expect(getSubCount(dep)).toBe(0)
  1068. })
  1069. it('should remove the dep when it is no longer used by the effect', () => {
  1070. const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
  1071. a: 1,
  1072. b: 2,
  1073. c: 'a',
  1074. })
  1075. effect(() => obj[obj.c])
  1076. const depC = getDepFromReactive(toRaw(obj), 'c')
  1077. expect(getSubCount(getDepFromReactive(toRaw(obj), 'a'))).toBe(1)
  1078. expect(getSubCount(depC)).toBe(1)
  1079. obj.c = 'b'
  1080. obj.a = 4
  1081. expect(getSubCount(getDepFromReactive(toRaw(obj), 'b'))).toBe(1)
  1082. expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
  1083. expect(getSubCount(depC)).toBe(1)
  1084. })
  1085. })
  1086. describe('onEffectCleanup', () => {
  1087. it('should get called correctly', async () => {
  1088. const count = ref(0)
  1089. const cleanupEffect = vi.fn()
  1090. const e = effect(() => {
  1091. onEffectCleanup(cleanupEffect)
  1092. count.value
  1093. })
  1094. count.value++
  1095. await nextTick()
  1096. expect(cleanupEffect).toHaveBeenCalledTimes(1)
  1097. count.value++
  1098. await nextTick()
  1099. expect(cleanupEffect).toHaveBeenCalledTimes(2)
  1100. // call it on stop
  1101. e.effect.stop()
  1102. expect(cleanupEffect).toHaveBeenCalledTimes(3)
  1103. })
  1104. it('should warn if called without active effect', () => {
  1105. onEffectCleanup(() => {})
  1106. expect(
  1107. `onEffectCleanup() was called when there was no active effect`,
  1108. ).toHaveBeenWarned()
  1109. })
  1110. it('should not warn without active effect when failSilently argument is passed', () => {
  1111. onEffectCleanup(() => {}, true)
  1112. expect(
  1113. `onEffectCleanup() was called when there was no active effect`,
  1114. ).not.toHaveBeenWarned()
  1115. })
  1116. })
  1117. test('should pause/resume effect', () => {
  1118. const obj = reactive({ foo: 1 })
  1119. const fnSpy = vi.fn(() => obj.foo)
  1120. const runner = effect(fnSpy)
  1121. expect(fnSpy).toHaveBeenCalledTimes(1)
  1122. expect(obj.foo).toBe(1)
  1123. runner.effect.pause()
  1124. obj.foo++
  1125. expect(fnSpy).toHaveBeenCalledTimes(1)
  1126. expect(obj.foo).toBe(2)
  1127. runner.effect.resume()
  1128. expect(fnSpy).toHaveBeenCalledTimes(2)
  1129. expect(obj.foo).toBe(2)
  1130. obj.foo++
  1131. expect(fnSpy).toHaveBeenCalledTimes(3)
  1132. expect(obj.foo).toBe(3)
  1133. })
  1134. test('should be executed once immediately when resume is called', () => {
  1135. const obj = reactive({ foo: 1 })
  1136. const fnSpy = vi.fn(() => obj.foo)
  1137. const runner = effect(fnSpy)
  1138. expect(fnSpy).toHaveBeenCalledTimes(1)
  1139. expect(obj.foo).toBe(1)
  1140. runner.effect.pause()
  1141. obj.foo++
  1142. expect(fnSpy).toHaveBeenCalledTimes(1)
  1143. expect(obj.foo).toBe(2)
  1144. obj.foo++
  1145. expect(fnSpy).toHaveBeenCalledTimes(1)
  1146. expect(obj.foo).toBe(3)
  1147. runner.effect.resume()
  1148. expect(fnSpy).toHaveBeenCalledTimes(2)
  1149. expect(obj.foo).toBe(3)
  1150. })
  1151. })