effect.spec.ts 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115
  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 support manipulating an array while observing symbol keyed properties', () => {
  221. const key = Symbol()
  222. let dummy
  223. const array: any = reactive([1, 2, 3])
  224. effect(() => (dummy = array[key]))
  225. expect(dummy).toBe(undefined)
  226. array.pop()
  227. array.shift()
  228. array.splice(0, 1)
  229. expect(dummy).toBe(undefined)
  230. array[key] = 'value'
  231. array.length = 0
  232. expect(dummy).toBe('value')
  233. })
  234. it('should observe function valued properties', () => {
  235. const oldFunc = () => {}
  236. const newFunc = () => {}
  237. let dummy
  238. const obj = reactive({ func: oldFunc })
  239. effect(() => (dummy = obj.func))
  240. expect(dummy).toBe(oldFunc)
  241. obj.func = newFunc
  242. expect(dummy).toBe(newFunc)
  243. })
  244. it('should observe chained getters relying on this', () => {
  245. const obj = reactive({
  246. a: 1,
  247. get b() {
  248. return this.a
  249. },
  250. })
  251. let dummy
  252. effect(() => (dummy = obj.b))
  253. expect(dummy).toBe(1)
  254. obj.a++
  255. expect(dummy).toBe(2)
  256. })
  257. it('should observe methods relying on this', () => {
  258. const obj = reactive({
  259. a: 1,
  260. 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 not observe set operations without a value change', () => {
  271. let hasDummy, getDummy
  272. const obj = reactive({ prop: 'value' })
  273. const getSpy = vi.fn(() => (getDummy = obj.prop))
  274. const hasSpy = vi.fn(() => (hasDummy = 'prop' in obj))
  275. effect(getSpy)
  276. effect(hasSpy)
  277. expect(getDummy).toBe('value')
  278. expect(hasDummy).toBe(true)
  279. obj.prop = 'value'
  280. expect(getSpy).toHaveBeenCalledTimes(1)
  281. expect(hasSpy).toHaveBeenCalledTimes(1)
  282. expect(getDummy).toBe('value')
  283. expect(hasDummy).toBe(true)
  284. })
  285. it('should not observe raw mutations', () => {
  286. let dummy
  287. const obj = reactive<{ prop?: string }>({})
  288. effect(() => (dummy = toRaw(obj).prop))
  289. expect(dummy).toBe(undefined)
  290. obj.prop = 'value'
  291. expect(dummy).toBe(undefined)
  292. })
  293. it('should not be triggered by raw mutations', () => {
  294. let dummy
  295. const obj = reactive<{ prop?: string }>({})
  296. effect(() => (dummy = obj.prop))
  297. expect(dummy).toBe(undefined)
  298. toRaw(obj).prop = 'value'
  299. expect(dummy).toBe(undefined)
  300. })
  301. it('should not be triggered by inherited raw setters', () => {
  302. let dummy, parentDummy, hiddenValue: any
  303. const obj = reactive<{ prop?: number }>({})
  304. const parent = reactive({
  305. set prop(value) {
  306. hiddenValue = value
  307. },
  308. get prop() {
  309. return hiddenValue
  310. },
  311. })
  312. Object.setPrototypeOf(obj, parent)
  313. effect(() => (dummy = obj.prop))
  314. effect(() => (parentDummy = parent.prop))
  315. expect(dummy).toBe(undefined)
  316. expect(parentDummy).toBe(undefined)
  317. toRaw(obj).prop = 4
  318. expect(dummy).toBe(undefined)
  319. expect(parentDummy).toBe(undefined)
  320. })
  321. it('should avoid implicit infinite recursive loops with itself', () => {
  322. const counter = reactive({ num: 0 })
  323. const counterSpy = vi.fn(() => counter.num++)
  324. effect(counterSpy)
  325. expect(counter.num).toBe(1)
  326. expect(counterSpy).toHaveBeenCalledTimes(1)
  327. counter.num = 4
  328. expect(counter.num).toBe(5)
  329. expect(counterSpy).toHaveBeenCalledTimes(2)
  330. })
  331. it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => {
  332. ;(['push', 'unshift'] as const).forEach(key => {
  333. const arr = reactive<number[]>([])
  334. const counterSpy1 = vi.fn(() => (arr[key] as any)(1))
  335. const counterSpy2 = vi.fn(() => (arr[key] as any)(2))
  336. effect(counterSpy1)
  337. effect(counterSpy2)
  338. expect(arr.length).toBe(2)
  339. expect(counterSpy1).toHaveBeenCalledTimes(1)
  340. expect(counterSpy2).toHaveBeenCalledTimes(1)
  341. })
  342. ;(['pop', 'shift'] as const).forEach(key => {
  343. const arr = reactive<number[]>([1, 2, 3, 4])
  344. const counterSpy1 = vi.fn(() => (arr[key] as any)())
  345. const counterSpy2 = vi.fn(() => (arr[key] as any)())
  346. effect(counterSpy1)
  347. effect(counterSpy2)
  348. expect(arr.length).toBe(2)
  349. expect(counterSpy1).toHaveBeenCalledTimes(1)
  350. expect(counterSpy2).toHaveBeenCalledTimes(1)
  351. })
  352. })
  353. it('should allow explicitly recursive raw function loops', () => {
  354. const counter = reactive({ num: 0 })
  355. const numSpy = vi.fn(() => {
  356. counter.num++
  357. if (counter.num < 10) {
  358. numSpy()
  359. }
  360. })
  361. effect(numSpy)
  362. expect(counter.num).toEqual(10)
  363. expect(numSpy).toHaveBeenCalledTimes(10)
  364. })
  365. it('should avoid infinite loops with other effects', () => {
  366. const nums = reactive({ num1: 0, num2: 1 })
  367. const spy1 = vi.fn(() => (nums.num1 = nums.num2))
  368. const spy2 = vi.fn(() => (nums.num2 = nums.num1))
  369. effect(spy1)
  370. effect(spy2)
  371. expect(nums.num1).toBe(1)
  372. expect(nums.num2).toBe(1)
  373. expect(spy1).toHaveBeenCalledTimes(1)
  374. expect(spy2).toHaveBeenCalledTimes(1)
  375. nums.num2 = 4
  376. expect(nums.num1).toBe(4)
  377. expect(nums.num2).toBe(4)
  378. expect(spy1).toHaveBeenCalledTimes(2)
  379. expect(spy2).toHaveBeenCalledTimes(2)
  380. nums.num1 = 10
  381. expect(nums.num1).toBe(10)
  382. expect(nums.num2).toBe(10)
  383. expect(spy1).toHaveBeenCalledTimes(3)
  384. expect(spy2).toHaveBeenCalledTimes(3)
  385. })
  386. it('should return a new reactive version of the function', () => {
  387. function greet() {
  388. return 'Hello World'
  389. }
  390. const effect1 = effect(greet)
  391. const effect2 = effect(greet)
  392. expect(typeof effect1).toBe('function')
  393. expect(typeof effect2).toBe('function')
  394. expect(effect1).not.toBe(greet)
  395. expect(effect1).not.toBe(effect2)
  396. })
  397. it('should discover new branches while running automatically', () => {
  398. let dummy
  399. const obj = reactive({ prop: 'value', run: false })
  400. const conditionalSpy = vi.fn(() => {
  401. dummy = obj.run ? obj.prop : 'other'
  402. })
  403. effect(conditionalSpy)
  404. expect(dummy).toBe('other')
  405. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  406. obj.prop = 'Hi'
  407. expect(dummy).toBe('other')
  408. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  409. obj.run = true
  410. expect(dummy).toBe('Hi')
  411. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  412. obj.prop = 'World'
  413. expect(dummy).toBe('World')
  414. expect(conditionalSpy).toHaveBeenCalledTimes(3)
  415. })
  416. it('should discover new branches when running manually', () => {
  417. let dummy
  418. let run = false
  419. const obj = reactive({ prop: 'value' })
  420. const runner = effect(() => {
  421. dummy = run ? obj.prop : 'other'
  422. })
  423. expect(dummy).toBe('other')
  424. runner()
  425. expect(dummy).toBe('other')
  426. run = true
  427. runner()
  428. expect(dummy).toBe('value')
  429. obj.prop = 'World'
  430. expect(dummy).toBe('World')
  431. })
  432. it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  433. let dummy
  434. const obj = reactive({ prop: 'value', run: true })
  435. const conditionalSpy = vi.fn(() => {
  436. dummy = obj.run ? obj.prop : 'other'
  437. })
  438. effect(conditionalSpy)
  439. expect(dummy).toBe('value')
  440. expect(conditionalSpy).toHaveBeenCalledTimes(1)
  441. obj.run = false
  442. expect(dummy).toBe('other')
  443. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  444. obj.prop = 'value2'
  445. expect(dummy).toBe('other')
  446. expect(conditionalSpy).toHaveBeenCalledTimes(2)
  447. })
  448. it('should handle deep effect recursion using cleanup fallback', () => {
  449. const results = reactive([0])
  450. const effects: { fx: ReactiveEffectRunner; index: number }[] = []
  451. for (let i = 1; i < 40; i++) {
  452. ;(index => {
  453. const fx = effect(() => {
  454. results[index] = results[index - 1] * 2
  455. })
  456. effects.push({ fx, index })
  457. })(i)
  458. }
  459. expect(results[39]).toBe(0)
  460. results[0] = 1
  461. expect(results[39]).toBe(Math.pow(2, 39))
  462. })
  463. it('should register deps independently during effect recursion', () => {
  464. const input = reactive({ a: 1, b: 2, c: 0 })
  465. const output = reactive({ fx1: 0, fx2: 0 })
  466. const fx1Spy = vi.fn(() => {
  467. let result = 0
  468. if (input.c < 2) result += input.a
  469. if (input.c > 1) result += input.b
  470. output.fx1 = result
  471. })
  472. const fx1 = effect(fx1Spy)
  473. const fx2Spy = vi.fn(() => {
  474. let result = 0
  475. if (input.c > 1) result += input.a
  476. if (input.c < 3) result += input.b
  477. output.fx2 = result + output.fx1
  478. })
  479. const fx2 = effect(fx2Spy)
  480. expect(fx1).not.toBeNull()
  481. expect(fx2).not.toBeNull()
  482. expect(output.fx1).toBe(1)
  483. expect(output.fx2).toBe(2 + 1)
  484. expect(fx1Spy).toHaveBeenCalledTimes(1)
  485. expect(fx2Spy).toHaveBeenCalledTimes(1)
  486. fx1Spy.mockClear()
  487. fx2Spy.mockClear()
  488. input.b = 3
  489. expect(output.fx1).toBe(1)
  490. expect(output.fx2).toBe(3 + 1)
  491. expect(fx1Spy).toHaveBeenCalledTimes(0)
  492. expect(fx2Spy).toHaveBeenCalledTimes(1)
  493. fx1Spy.mockClear()
  494. fx2Spy.mockClear()
  495. input.c = 1
  496. expect(output.fx1).toBe(1)
  497. expect(output.fx2).toBe(3 + 1)
  498. expect(fx1Spy).toHaveBeenCalledTimes(1)
  499. expect(fx2Spy).toHaveBeenCalledTimes(1)
  500. fx1Spy.mockClear()
  501. fx2Spy.mockClear()
  502. input.c = 2
  503. expect(output.fx1).toBe(3)
  504. expect(output.fx2).toBe(1 + 3 + 3)
  505. expect(fx1Spy).toHaveBeenCalledTimes(1)
  506. // Invoked due to change of fx1.
  507. expect(fx2Spy).toHaveBeenCalledTimes(1)
  508. fx1Spy.mockClear()
  509. fx2Spy.mockClear()
  510. input.c = 3
  511. expect(output.fx1).toBe(3)
  512. expect(output.fx2).toBe(1 + 3)
  513. expect(fx1Spy).toHaveBeenCalledTimes(1)
  514. expect(fx2Spy).toHaveBeenCalledTimes(1)
  515. fx1Spy.mockClear()
  516. fx2Spy.mockClear()
  517. input.a = 10
  518. expect(output.fx1).toBe(3)
  519. expect(output.fx2).toBe(10 + 3)
  520. expect(fx1Spy).toHaveBeenCalledTimes(0)
  521. expect(fx2Spy).toHaveBeenCalledTimes(1)
  522. })
  523. it('should not double wrap if the passed function is a effect', () => {
  524. const runner = effect(() => {})
  525. const otherRunner = effect(runner)
  526. expect(runner).not.toBe(otherRunner)
  527. expect(runner.effect.fn).toBe(otherRunner.effect.fn)
  528. })
  529. it('should wrap if the passed function is a fake effect', () => {
  530. const fakeRunner = () => {}
  531. fakeRunner.effect = {}
  532. const runner = effect(fakeRunner)
  533. expect(fakeRunner).not.toBe(runner)
  534. expect(runner.effect.fn).toBe(fakeRunner)
  535. })
  536. it('should not run multiple times for a single mutation', () => {
  537. let dummy
  538. const obj = reactive<Record<string, number>>({})
  539. const fnSpy = vi.fn(() => {
  540. for (const key in obj) {
  541. dummy = obj[key]
  542. }
  543. dummy = obj.prop
  544. })
  545. effect(fnSpy)
  546. expect(fnSpy).toHaveBeenCalledTimes(1)
  547. obj.prop = 16
  548. expect(dummy).toBe(16)
  549. expect(fnSpy).toHaveBeenCalledTimes(2)
  550. })
  551. it('should allow nested effects', () => {
  552. const nums = reactive({ num1: 0, num2: 1, num3: 2 })
  553. const dummy: any = {}
  554. const childSpy = vi.fn(() => (dummy.num1 = nums.num1))
  555. const childeffect = effect(childSpy)
  556. const parentSpy = vi.fn(() => {
  557. dummy.num2 = nums.num2
  558. childeffect()
  559. dummy.num3 = nums.num3
  560. })
  561. effect(parentSpy)
  562. expect(dummy).toEqual({ num1: 0, num2: 1, num3: 2 })
  563. expect(parentSpy).toHaveBeenCalledTimes(1)
  564. expect(childSpy).toHaveBeenCalledTimes(2)
  565. // this should only call the childeffect
  566. nums.num1 = 4
  567. expect(dummy).toEqual({ num1: 4, num2: 1, num3: 2 })
  568. expect(parentSpy).toHaveBeenCalledTimes(1)
  569. expect(childSpy).toHaveBeenCalledTimes(3)
  570. // this calls the parenteffect, which calls the childeffect once
  571. nums.num2 = 10
  572. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 2 })
  573. expect(parentSpy).toHaveBeenCalledTimes(2)
  574. expect(childSpy).toHaveBeenCalledTimes(4)
  575. // this calls the parenteffect, which calls the childeffect once
  576. nums.num3 = 7
  577. expect(dummy).toEqual({ num1: 4, num2: 10, num3: 7 })
  578. expect(parentSpy).toHaveBeenCalledTimes(3)
  579. expect(childSpy).toHaveBeenCalledTimes(5)
  580. })
  581. it('should observe json methods', () => {
  582. let dummy = <Record<string, number>>{}
  583. const obj = reactive<Record<string, number>>({})
  584. effect(() => {
  585. dummy = JSON.parse(JSON.stringify(obj))
  586. })
  587. obj.a = 1
  588. expect(dummy.a).toBe(1)
  589. })
  590. it('should observe class method invocations', () => {
  591. class Model {
  592. count: number
  593. constructor() {
  594. this.count = 0
  595. }
  596. inc() {
  597. this.count++
  598. }
  599. }
  600. const model = reactive(new Model())
  601. let dummy
  602. effect(() => {
  603. dummy = model.count
  604. })
  605. expect(dummy).toBe(0)
  606. model.inc()
  607. expect(dummy).toBe(1)
  608. })
  609. it('lazy', () => {
  610. const obj = reactive({ foo: 1 })
  611. let dummy
  612. const runner = effect(() => (dummy = obj.foo), { lazy: true })
  613. expect(dummy).toBe(undefined)
  614. expect(runner()).toBe(1)
  615. expect(dummy).toBe(1)
  616. obj.foo = 2
  617. expect(dummy).toBe(2)
  618. })
  619. it('scheduler', () => {
  620. let dummy
  621. let run: any
  622. const scheduler = vi.fn(() => {
  623. run = runner
  624. })
  625. const obj = reactive({ foo: 1 })
  626. const runner = effect(
  627. () => {
  628. dummy = obj.foo
  629. },
  630. { scheduler },
  631. )
  632. expect(scheduler).not.toHaveBeenCalled()
  633. expect(dummy).toBe(1)
  634. // should be called on first trigger
  635. obj.foo++
  636. expect(scheduler).toHaveBeenCalledTimes(1)
  637. // should not run yet
  638. expect(dummy).toBe(1)
  639. // manually run
  640. run()
  641. // should have run
  642. expect(dummy).toBe(2)
  643. })
  644. it('events: onTrack', () => {
  645. let events: DebuggerEvent[] = []
  646. let dummy
  647. const onTrack = vi.fn((e: DebuggerEvent) => {
  648. events.push(e)
  649. })
  650. const obj = reactive({ foo: 1, bar: 2 })
  651. const runner = effect(
  652. () => {
  653. dummy = obj.foo
  654. dummy = 'bar' in obj
  655. dummy = Object.keys(obj)
  656. },
  657. { onTrack },
  658. )
  659. expect(dummy).toEqual(['foo', 'bar'])
  660. expect(onTrack).toHaveBeenCalledTimes(3)
  661. expect(events).toEqual([
  662. {
  663. effect: runner.effect,
  664. target: toRaw(obj),
  665. type: TrackOpTypes.GET,
  666. key: 'foo',
  667. },
  668. {
  669. effect: runner.effect,
  670. target: toRaw(obj),
  671. type: TrackOpTypes.HAS,
  672. key: 'bar',
  673. },
  674. {
  675. effect: runner.effect,
  676. target: toRaw(obj),
  677. type: TrackOpTypes.ITERATE,
  678. key: ITERATE_KEY,
  679. },
  680. ])
  681. })
  682. it('events: onTrigger', () => {
  683. let events: DebuggerEvent[] = []
  684. let dummy
  685. const onTrigger = vi.fn((e: DebuggerEvent) => {
  686. events.push(e)
  687. })
  688. const obj = reactive<{ foo?: number }>({ foo: 1 })
  689. const runner = effect(
  690. () => {
  691. dummy = obj.foo
  692. },
  693. { onTrigger },
  694. )
  695. obj.foo!++
  696. expect(dummy).toBe(2)
  697. expect(onTrigger).toHaveBeenCalledTimes(1)
  698. expect(events[0]).toEqual({
  699. effect: runner.effect,
  700. target: toRaw(obj),
  701. type: TriggerOpTypes.SET,
  702. key: 'foo',
  703. oldValue: 1,
  704. newValue: 2,
  705. })
  706. delete obj.foo
  707. expect(dummy).toBeUndefined()
  708. expect(onTrigger).toHaveBeenCalledTimes(2)
  709. expect(events[1]).toEqual({
  710. effect: runner.effect,
  711. target: toRaw(obj),
  712. type: TriggerOpTypes.DELETE,
  713. key: 'foo',
  714. oldValue: 2,
  715. })
  716. })
  717. it('stop', () => {
  718. let dummy
  719. const obj = reactive({ prop: 1 })
  720. const runner = effect(() => {
  721. dummy = obj.prop
  722. })
  723. obj.prop = 2
  724. expect(dummy).toBe(2)
  725. stop(runner)
  726. obj.prop = 3
  727. expect(dummy).toBe(2)
  728. // stopped effect should still be manually callable
  729. runner()
  730. expect(dummy).toBe(3)
  731. })
  732. it('events: onStop', () => {
  733. const onStop = vi.fn()
  734. const runner = effect(() => {}, {
  735. onStop,
  736. })
  737. stop(runner)
  738. expect(onStop).toHaveBeenCalled()
  739. })
  740. it('stop: a stopped effect is nested in a normal effect', () => {
  741. let dummy
  742. const obj = reactive({ prop: 1 })
  743. const runner = effect(() => {
  744. dummy = obj.prop
  745. })
  746. stop(runner)
  747. obj.prop = 2
  748. expect(dummy).toBe(1)
  749. // observed value in inner stopped effect
  750. // will track outer effect as an dependency
  751. effect(() => {
  752. runner()
  753. })
  754. expect(dummy).toBe(2)
  755. // notify outer effect to run
  756. obj.prop = 3
  757. expect(dummy).toBe(3)
  758. })
  759. it('markRaw', () => {
  760. const obj = reactive({
  761. foo: markRaw({
  762. prop: 0,
  763. }),
  764. })
  765. let dummy
  766. effect(() => {
  767. dummy = obj.foo.prop
  768. })
  769. expect(dummy).toBe(0)
  770. obj.foo.prop++
  771. expect(dummy).toBe(0)
  772. obj.foo = { prop: 1 }
  773. expect(dummy).toBe(1)
  774. })
  775. it('should not be triggered when the value and the old value both are NaN', () => {
  776. const obj = reactive({
  777. foo: NaN,
  778. })
  779. const fnSpy = vi.fn(() => obj.foo)
  780. effect(fnSpy)
  781. obj.foo = NaN
  782. expect(fnSpy).toHaveBeenCalledTimes(1)
  783. })
  784. it('should trigger all effects when array length is set to 0', () => {
  785. const observed: any = reactive([1])
  786. let dummy, record
  787. effect(() => {
  788. dummy = observed.length
  789. })
  790. effect(() => {
  791. record = observed[0]
  792. })
  793. expect(dummy).toBe(1)
  794. expect(record).toBe(1)
  795. observed[1] = 2
  796. expect(observed[1]).toBe(2)
  797. observed.unshift(3)
  798. expect(dummy).toBe(3)
  799. expect(record).toBe(3)
  800. observed.length = 0
  801. expect(dummy).toBe(0)
  802. expect(record).toBeUndefined()
  803. })
  804. it('should not be triggered when set with the same proxy', () => {
  805. const obj = reactive({ foo: 1 })
  806. const observed: any = reactive({ obj })
  807. const fnSpy = vi.fn(() => observed.obj)
  808. effect(fnSpy)
  809. expect(fnSpy).toHaveBeenCalledTimes(1)
  810. observed.obj = obj
  811. expect(fnSpy).toHaveBeenCalledTimes(1)
  812. const obj2 = reactive({ foo: 1 })
  813. const observed2: any = shallowReactive({ obj2 })
  814. const fnSpy2 = vi.fn(() => observed2.obj2)
  815. effect(fnSpy2)
  816. expect(fnSpy2).toHaveBeenCalledTimes(1)
  817. observed2.obj2 = obj2
  818. expect(fnSpy2).toHaveBeenCalledTimes(1)
  819. })
  820. it('should be triggered when set length with string', () => {
  821. let ret1 = 'idle'
  822. let ret2 = 'idle'
  823. const arr1 = reactive(new Array(11).fill(0))
  824. const arr2 = reactive(new Array(11).fill(0))
  825. effect(() => {
  826. ret1 = arr1[10] === undefined ? 'arr[10] is set to empty' : 'idle'
  827. })
  828. effect(() => {
  829. ret2 = arr2[10] === undefined ? 'arr[10] is set to empty' : 'idle'
  830. })
  831. arr1.length = 2
  832. arr2.length = '2' as any
  833. expect(ret1).toBe(ret2)
  834. })
  835. describe('readonly + reactive for Map', () => {
  836. test('should work with readonly(reactive(Map))', () => {
  837. const m = reactive(new Map())
  838. const roM = readonly(m)
  839. const fnSpy = vi.fn(() => roM.get(1))
  840. effect(fnSpy)
  841. expect(fnSpy).toHaveBeenCalledTimes(1)
  842. m.set(1, 1)
  843. expect(fnSpy).toHaveBeenCalledTimes(2)
  844. })
  845. test('should work with observed value as key', () => {
  846. const key = reactive({})
  847. const m = reactive(new Map())
  848. m.set(key, 1)
  849. const roM = readonly(m)
  850. const fnSpy = vi.fn(() => roM.get(key))
  851. effect(fnSpy)
  852. expect(fnSpy).toHaveBeenCalledTimes(1)
  853. m.set(key, 1)
  854. expect(fnSpy).toHaveBeenCalledTimes(1)
  855. m.set(key, 2)
  856. expect(fnSpy).toHaveBeenCalledTimes(2)
  857. })
  858. test('should track hasOwnProperty', () => {
  859. const obj: any = reactive({})
  860. let has = false
  861. const fnSpy = vi.fn()
  862. effect(() => {
  863. fnSpy()
  864. has = obj.hasOwnProperty('foo')
  865. })
  866. expect(fnSpy).toHaveBeenCalledTimes(1)
  867. expect(has).toBe(false)
  868. obj.foo = 1
  869. expect(fnSpy).toHaveBeenCalledTimes(2)
  870. expect(has).toBe(true)
  871. delete obj.foo
  872. expect(fnSpy).toHaveBeenCalledTimes(3)
  873. expect(has).toBe(false)
  874. // should not trigger on unrelated key
  875. obj.bar = 2
  876. expect(fnSpy).toHaveBeenCalledTimes(3)
  877. expect(has).toBe(false)
  878. })
  879. })
  880. it('should be triggered once with pauseScheduling', () => {
  881. const counter = reactive({ num: 0 })
  882. const counterSpy = vi.fn(() => counter.num)
  883. effect(counterSpy)
  884. counterSpy.mockClear()
  885. pauseScheduling()
  886. counter.num++
  887. counter.num++
  888. resetScheduling()
  889. expect(counterSpy).toHaveBeenCalledTimes(1)
  890. })
  891. // #10082
  892. it('should set dirtyLevel when effect is allowRecurse and is running', async () => {
  893. const s = ref(0)
  894. const n = computed(() => s.value + 1)
  895. const Child = {
  896. setup() {
  897. s.value++
  898. return () => n.value
  899. },
  900. }
  901. const renderSpy = vi.fn()
  902. const Parent = {
  903. setup() {
  904. return () => {
  905. renderSpy()
  906. return [n.value, h(Child)]
  907. }
  908. },
  909. }
  910. const root = nodeOps.createElement('div')
  911. render(h(Parent), root)
  912. await nextTick()
  913. expect(serializeInner(root)).toBe('22')
  914. expect(renderSpy).toHaveBeenCalledTimes(2)
  915. })
  916. describe('empty dep cleanup', () => {
  917. it('should remove the dep when the effect is stopped', () => {
  918. const obj = reactive({ prop: 1 })
  919. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  920. const runner = effect(() => obj.prop)
  921. const dep = getDepFromReactive(toRaw(obj), 'prop')
  922. expect(dep).toHaveLength(1)
  923. obj.prop = 2
  924. expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
  925. expect(dep).toHaveLength(1)
  926. stop(runner)
  927. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  928. obj.prop = 3
  929. runner()
  930. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  931. })
  932. it('should only remove the dep when the last effect is stopped', () => {
  933. const obj = reactive({ prop: 1 })
  934. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  935. const runner1 = effect(() => obj.prop)
  936. const dep = getDepFromReactive(toRaw(obj), 'prop')
  937. expect(dep).toHaveLength(1)
  938. const runner2 = effect(() => obj.prop)
  939. expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
  940. expect(dep).toHaveLength(2)
  941. obj.prop = 2
  942. expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
  943. expect(dep).toHaveLength(2)
  944. stop(runner1)
  945. expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
  946. expect(dep).toHaveLength(1)
  947. obj.prop = 3
  948. expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
  949. expect(dep).toHaveLength(1)
  950. stop(runner2)
  951. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  952. obj.prop = 4
  953. runner1()
  954. runner2()
  955. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  956. })
  957. it('should remove the dep when it is no longer used by the effect', () => {
  958. const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
  959. a: 1,
  960. b: 2,
  961. c: 'a',
  962. })
  963. expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
  964. effect(() => obj[obj.c])
  965. const depC = getDepFromReactive(toRaw(obj), 'c')
  966. expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
  967. expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
  968. expect(depC).toHaveLength(1)
  969. obj.c = 'b'
  970. obj.a = 4
  971. expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
  972. expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
  973. expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
  974. expect(depC).toHaveLength(1)
  975. })
  976. })
  977. })