effect.spec.ts 30 KB

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