effect.spec.ts 28 KB

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