effect.spec.ts 30 KB

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