effectScope.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import { nextTick, watch, watchEffect } from '@vue/runtime-core'
  2. import {
  3. type ComputedRef,
  4. EffectScope,
  5. ReactiveEffect,
  6. computed,
  7. effect,
  8. effectScope,
  9. getCurrentScope,
  10. onScopeDispose,
  11. reactive,
  12. ref,
  13. setCurrentScope,
  14. } from '../src'
  15. describe('reactivity/effect/scope', () => {
  16. it('should run', () => {
  17. const fnSpy = vi.fn(() => {})
  18. effectScope().run(fnSpy)
  19. expect(fnSpy).toHaveBeenCalledTimes(1)
  20. })
  21. it('should accept zero argument', () => {
  22. const scope = effectScope()
  23. expect(getEffectsCount(scope)).toBe(0)
  24. })
  25. it('should return run value', () => {
  26. expect(effectScope().run(() => 1)).toBe(1)
  27. })
  28. it('should work w/ active property', () => {
  29. const scope = effectScope()
  30. const src = computed(() => 1)
  31. scope.run(() => src.value)
  32. expect(scope.active).toBe(true)
  33. scope.stop()
  34. expect(scope.active).toBe(false)
  35. })
  36. it('should collect the effects', () => {
  37. const scope = effectScope()
  38. scope.run(() => {
  39. let dummy
  40. const counter = reactive({ num: 0 })
  41. effect(() => (dummy = counter.num))
  42. expect(dummy).toBe(0)
  43. counter.num = 7
  44. expect(dummy).toBe(7)
  45. })
  46. expect(getEffectsCount(scope)).toBe(1)
  47. })
  48. it('stop', () => {
  49. let dummy, doubled
  50. const counter = reactive({ num: 0 })
  51. const scope = effectScope()
  52. scope.run(() => {
  53. effect(() => (dummy = counter.num))
  54. effect(() => (doubled = counter.num * 2))
  55. })
  56. expect(getEffectsCount(scope)).toBe(2)
  57. expect(dummy).toBe(0)
  58. counter.num = 7
  59. expect(dummy).toBe(7)
  60. expect(doubled).toBe(14)
  61. scope.stop()
  62. counter.num = 6
  63. expect(dummy).toBe(7)
  64. expect(doubled).toBe(14)
  65. })
  66. it('should collect nested scope', () => {
  67. let dummy, doubled
  68. const counter = reactive({ num: 0 })
  69. const scope = effectScope()
  70. scope.run(() => {
  71. effect(() => (dummy = counter.num))
  72. // nested scope
  73. effectScope().run(() => {
  74. effect(() => (doubled = counter.num * 2))
  75. })
  76. })
  77. expect(getEffectsCount(scope)).toBe(1)
  78. expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
  79. expect(dummy).toBe(0)
  80. counter.num = 7
  81. expect(dummy).toBe(7)
  82. expect(doubled).toBe(14)
  83. // stop the nested scope as well
  84. scope.stop()
  85. counter.num = 6
  86. expect(dummy).toBe(7)
  87. expect(doubled).toBe(14)
  88. })
  89. it('nested scope can be escaped', () => {
  90. let dummy, doubled
  91. const counter = reactive({ num: 0 })
  92. const scope = effectScope()
  93. scope.run(() => {
  94. effect(() => (dummy = counter.num))
  95. // nested scope
  96. effectScope(true).run(() => {
  97. effect(() => (doubled = counter.num * 2))
  98. })
  99. })
  100. expect(getEffectsCount(scope)).toBe(1)
  101. expect(dummy).toBe(0)
  102. counter.num = 7
  103. expect(dummy).toBe(7)
  104. expect(doubled).toBe(14)
  105. scope.stop()
  106. counter.num = 6
  107. expect(dummy).toBe(7)
  108. // nested scope should not be stopped
  109. expect(doubled).toBe(12)
  110. })
  111. it('able to run the scope', () => {
  112. let dummy, doubled
  113. const counter = reactive({ num: 0 })
  114. const scope = effectScope()
  115. scope.run(() => {
  116. effect(() => (dummy = counter.num))
  117. })
  118. expect(getEffectsCount(scope)).toBe(1)
  119. scope.run(() => {
  120. effect(() => (doubled = counter.num * 2))
  121. })
  122. expect(getEffectsCount(scope)).toBe(2)
  123. counter.num = 7
  124. expect(dummy).toBe(7)
  125. expect(doubled).toBe(14)
  126. scope.stop()
  127. })
  128. it('can not run an inactive scope', () => {
  129. let dummy, doubled
  130. const counter = reactive({ num: 0 })
  131. const scope = effectScope()
  132. scope.run(() => {
  133. effect(() => (dummy = counter.num))
  134. })
  135. expect(getEffectsCount(scope)).toBe(1)
  136. scope.stop()
  137. expect(getEffectsCount(scope)).toBe(0)
  138. scope.run(() => {
  139. effect(() => (doubled = counter.num * 2))
  140. })
  141. expect(getEffectsCount(scope)).toBe(1)
  142. counter.num = 7
  143. expect(dummy).toBe(0)
  144. expect(doubled).toBe(14)
  145. })
  146. it('should fire onScopeDispose hook', () => {
  147. let dummy = 0
  148. const scope = effectScope()
  149. scope.run(() => {
  150. onScopeDispose(() => (dummy += 1))
  151. onScopeDispose(() => (dummy += 2))
  152. })
  153. scope.run(() => {
  154. onScopeDispose(() => (dummy += 4))
  155. })
  156. expect(dummy).toBe(0)
  157. scope.stop()
  158. expect(dummy).toBe(7)
  159. })
  160. it('should warn onScopeDispose() is called when there is no active effect scope', () => {
  161. const spy = vi.fn()
  162. const scope = effectScope()
  163. scope.run(() => {
  164. onScopeDispose(spy)
  165. })
  166. expect(spy).toHaveBeenCalledTimes(0)
  167. onScopeDispose(spy)
  168. expect(
  169. '[Vue warn] onScopeDispose() is called when there is no active effect scope to be associated with.',
  170. ).toHaveBeenWarned()
  171. scope.stop()
  172. expect(spy).toHaveBeenCalledTimes(1)
  173. })
  174. it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
  175. const parent = effectScope()
  176. const child = parent.run(() => effectScope())!
  177. expect(parent.deps?.dep).toBe(child)
  178. child.stop()
  179. expect(parent.deps).toBeUndefined()
  180. })
  181. it('test with higher level APIs', async () => {
  182. const r = ref(1)
  183. const computedSpy = vi.fn()
  184. const watchSpy = vi.fn()
  185. const watchEffectSpy = vi.fn()
  186. let c: ComputedRef
  187. const scope = effectScope()
  188. scope.run(() => {
  189. c = computed(() => {
  190. computedSpy()
  191. return r.value + 1
  192. })
  193. watch(r, watchSpy)
  194. watchEffect(() => {
  195. watchEffectSpy()
  196. r.value
  197. c.value
  198. })
  199. })
  200. expect(computedSpy).toHaveBeenCalledTimes(1)
  201. expect(watchSpy).toHaveBeenCalledTimes(0)
  202. expect(watchEffectSpy).toHaveBeenCalledTimes(1)
  203. r.value++
  204. await nextTick()
  205. expect(computedSpy).toHaveBeenCalledTimes(2)
  206. expect(watchSpy).toHaveBeenCalledTimes(1)
  207. expect(watchEffectSpy).toHaveBeenCalledTimes(2)
  208. scope.stop()
  209. r.value++
  210. await nextTick()
  211. // should not trigger anymore
  212. expect(computedSpy).toHaveBeenCalledTimes(2)
  213. expect(watchSpy).toHaveBeenCalledTimes(1)
  214. expect(watchEffectSpy).toHaveBeenCalledTimes(2)
  215. })
  216. it('getCurrentScope() stays valid when running a detached nested EffectScope', () => {
  217. const parentScope = effectScope()
  218. parentScope.run(() => {
  219. const currentScope = getCurrentScope()
  220. expect(currentScope).toBeDefined()
  221. const detachedScope = effectScope(true)
  222. detachedScope.run(() => {})
  223. expect(getCurrentScope()).toBe(currentScope)
  224. })
  225. })
  226. it('calling .off() of a detached scope inside an active scope should not break currentScope', () => {
  227. const parentScope = effectScope()
  228. parentScope.run(() => {
  229. const childScope = effectScope(true)
  230. setCurrentScope(setCurrentScope(childScope))
  231. expect(getCurrentScope()).toBe(parentScope)
  232. })
  233. })
  234. it('should pause/resume EffectScope', async () => {
  235. const counter = reactive({ num: 0 })
  236. const fnSpy = vi.fn(() => counter.num)
  237. const scope = new EffectScope()
  238. scope.run(() => {
  239. effect(fnSpy)
  240. })
  241. expect(fnSpy).toHaveBeenCalledTimes(1)
  242. counter.num++
  243. await nextTick()
  244. expect(fnSpy).toHaveBeenCalledTimes(2)
  245. scope.pause()
  246. counter.num++
  247. await nextTick()
  248. expect(fnSpy).toHaveBeenCalledTimes(2)
  249. counter.num++
  250. await nextTick()
  251. expect(fnSpy).toHaveBeenCalledTimes(2)
  252. scope.resume()
  253. expect(fnSpy).toHaveBeenCalledTimes(3)
  254. })
  255. test('removing a watcher while stopping its effectScope', async () => {
  256. const count = ref(0)
  257. const scope = effectScope()
  258. let watcherCalls = 0
  259. let cleanupCalls = 0
  260. scope.run(() => {
  261. const stop1 = watch(count, () => {
  262. watcherCalls++
  263. })
  264. watch(count, (val, old, onCleanup) => {
  265. watcherCalls++
  266. onCleanup(() => {
  267. cleanupCalls++
  268. stop1()
  269. })
  270. })
  271. watch(count, () => {
  272. watcherCalls++
  273. })
  274. })
  275. expect(watcherCalls).toBe(0)
  276. expect(cleanupCalls).toBe(0)
  277. count.value++
  278. await nextTick()
  279. expect(watcherCalls).toBe(3)
  280. expect(cleanupCalls).toBe(0)
  281. scope.stop()
  282. count.value++
  283. await nextTick()
  284. expect(watcherCalls).toBe(3)
  285. expect(cleanupCalls).toBe(1)
  286. expect(getEffectsCount(scope)).toBe(0)
  287. expect(scope.cleanupsLength).toBe(0)
  288. })
  289. it('should still trigger updates after stopping scope stored in reactive object', () => {
  290. const rs = ref({
  291. stage: 0,
  292. scope: null as any,
  293. })
  294. let renderCount = 0
  295. effect(() => {
  296. renderCount++
  297. return rs.value.stage
  298. })
  299. const handleBegin = () => {
  300. const status = rs.value
  301. status.stage = 1
  302. status.scope = effectScope()
  303. status.scope.run(() => {
  304. watch([() => status.stage], () => {})
  305. })
  306. }
  307. const handleExit = () => {
  308. const status = rs.value
  309. status.stage = 0
  310. const watchScope = status.scope
  311. status.scope = null
  312. if (watchScope) {
  313. watchScope.stop()
  314. }
  315. }
  316. expect(rs.value.stage).toBe(0)
  317. expect(renderCount).toBe(1)
  318. // 1. Click begin
  319. handleBegin()
  320. expect(rs.value.stage).toBe(1)
  321. expect(renderCount).toBe(2)
  322. // 2. Click add
  323. rs.value.stage++
  324. expect(rs.value.stage).toBe(2)
  325. expect(renderCount).toBe(3)
  326. // 3. Click end
  327. handleExit()
  328. expect(rs.value.stage).toBe(0)
  329. expect(renderCount).toBe(4)
  330. handleBegin()
  331. expect(rs.value.stage).toBe(1)
  332. expect(renderCount).toBe(5)
  333. })
  334. it('should still trigger updates after stopping scope stored in reactive object', () => {
  335. const rs = ref({
  336. stage: 0,
  337. scope: null as any,
  338. })
  339. let renderCount = 0
  340. effect(() => {
  341. renderCount++
  342. return rs.value.stage
  343. })
  344. const handleBegin = () => {
  345. const status = rs.value
  346. status.stage = 1
  347. status.scope = effectScope()
  348. status.scope.run(() => {
  349. watch([() => status.stage], () => {})
  350. })
  351. }
  352. const handleExit = () => {
  353. const status = rs.value
  354. status.stage = 0
  355. const watchScope = status.scope
  356. status.scope = null
  357. if (watchScope) {
  358. watchScope.stop()
  359. }
  360. }
  361. expect(rs.value.stage).toBe(0)
  362. expect(renderCount).toBe(1)
  363. // 1. Click begin
  364. handleBegin()
  365. expect(rs.value.stage).toBe(1)
  366. expect(renderCount).toBe(2)
  367. // 2. Click add
  368. rs.value.stage++
  369. expect(rs.value.stage).toBe(2)
  370. expect(renderCount).toBe(3)
  371. // 3. Click end
  372. handleExit()
  373. expect(rs.value.stage).toBe(0)
  374. expect(renderCount).toBe(4)
  375. handleBegin()
  376. expect(rs.value.stage).toBe(1)
  377. expect(renderCount).toBe(5)
  378. })
  379. })
  380. function getEffectsCount(scope: EffectScope): number {
  381. let n = 0
  382. for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
  383. if (dep.dep instanceof ReactiveEffect) {
  384. n++
  385. }
  386. }
  387. return n
  388. }