effect.spec.ts 33 KB

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