effect.spec.ts 25 KB

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