effectScope.spec.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { nextTick, watch, watchEffect } from '@vue/runtime-core'
  2. import {
  3. reactive,
  4. effect,
  5. EffectScope,
  6. onScopeDispose,
  7. computed,
  8. ref,
  9. ComputedRef
  10. } from '../src'
  11. describe('reactivity/effect/scope', () => {
  12. it('should run', () => {
  13. const fnSpy = jest.fn(() => {})
  14. new EffectScope().run(fnSpy)
  15. expect(fnSpy).toHaveBeenCalledTimes(1)
  16. })
  17. it('should accept zero argument', () => {
  18. const scope = new EffectScope()
  19. expect(scope.effects.length).toBe(0)
  20. })
  21. it('should return run value', () => {
  22. expect(new EffectScope().run(() => 1)).toBe(1)
  23. })
  24. it('should collect the effects', () => {
  25. const scope = new EffectScope()
  26. scope.run(() => {
  27. let dummy
  28. const counter = reactive({ num: 0 })
  29. effect(() => (dummy = counter.num))
  30. expect(dummy).toBe(0)
  31. counter.num = 7
  32. expect(dummy).toBe(7)
  33. })
  34. expect(scope.effects.length).toBe(1)
  35. })
  36. it('stop', () => {
  37. let dummy, doubled
  38. const counter = reactive({ num: 0 })
  39. const scope = new EffectScope()
  40. scope.run(() => {
  41. effect(() => (dummy = counter.num))
  42. effect(() => (doubled = counter.num * 2))
  43. })
  44. expect(scope.effects.length).toBe(2)
  45. expect(dummy).toBe(0)
  46. counter.num = 7
  47. expect(dummy).toBe(7)
  48. expect(doubled).toBe(14)
  49. scope.stop()
  50. counter.num = 6
  51. expect(dummy).toBe(7)
  52. expect(doubled).toBe(14)
  53. })
  54. it('should collect nested scope', () => {
  55. let dummy, doubled
  56. const counter = reactive({ num: 0 })
  57. const scope = new EffectScope()
  58. scope.run(() => {
  59. effect(() => (dummy = counter.num))
  60. // nested scope
  61. new EffectScope().run(() => {
  62. effect(() => (doubled = counter.num * 2))
  63. })
  64. })
  65. expect(scope.effects.length).toBe(1)
  66. expect(scope.scopes!.length).toBe(1)
  67. expect(scope.scopes![0]).toBeInstanceOf(EffectScope)
  68. expect(dummy).toBe(0)
  69. counter.num = 7
  70. expect(dummy).toBe(7)
  71. expect(doubled).toBe(14)
  72. // stop the nested scope as well
  73. scope.stop()
  74. counter.num = 6
  75. expect(dummy).toBe(7)
  76. expect(doubled).toBe(14)
  77. })
  78. it('nested scope can be escaped', () => {
  79. let dummy, doubled
  80. const counter = reactive({ num: 0 })
  81. const scope = new EffectScope()
  82. scope.run(() => {
  83. effect(() => (dummy = counter.num))
  84. // nested scope
  85. new EffectScope(true).run(() => {
  86. effect(() => (doubled = counter.num * 2))
  87. })
  88. })
  89. expect(scope.effects.length).toBe(1)
  90. expect(dummy).toBe(0)
  91. counter.num = 7
  92. expect(dummy).toBe(7)
  93. expect(doubled).toBe(14)
  94. scope.stop()
  95. counter.num = 6
  96. expect(dummy).toBe(7)
  97. // nested scope should not be stoped
  98. expect(doubled).toBe(12)
  99. })
  100. it('able to run the scope', () => {
  101. let dummy, doubled
  102. const counter = reactive({ num: 0 })
  103. const scope = new EffectScope()
  104. scope.run(() => {
  105. effect(() => (dummy = counter.num))
  106. })
  107. expect(scope.effects.length).toBe(1)
  108. scope.run(() => {
  109. effect(() => (doubled = counter.num * 2))
  110. })
  111. expect(scope.effects.length).toBe(2)
  112. counter.num = 7
  113. expect(dummy).toBe(7)
  114. expect(doubled).toBe(14)
  115. scope.stop()
  116. })
  117. it('can not run an inactive scope', () => {
  118. let dummy, doubled
  119. const counter = reactive({ num: 0 })
  120. const scope = new EffectScope()
  121. scope.run(() => {
  122. effect(() => (dummy = counter.num))
  123. })
  124. expect(scope.effects.length).toBe(1)
  125. scope.stop()
  126. scope.run(() => {
  127. effect(() => (doubled = counter.num * 2))
  128. })
  129. expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
  130. expect(scope.effects.length).toBe(1)
  131. counter.num = 7
  132. expect(dummy).toBe(0)
  133. expect(doubled).toBe(undefined)
  134. })
  135. it('should fire onScopeDispose hook', () => {
  136. let dummy = 0
  137. const scope = new EffectScope()
  138. scope.run(() => {
  139. onScopeDispose(() => (dummy += 1))
  140. onScopeDispose(() => (dummy += 2))
  141. })
  142. scope.run(() => {
  143. onScopeDispose(() => (dummy += 4))
  144. })
  145. expect(dummy).toBe(0)
  146. scope.stop()
  147. expect(dummy).toBe(7)
  148. })
  149. it('should warn onScopeDispose() is called when there is no active effect scope', () => {
  150. const spy = jest.fn()
  151. const scope = new EffectScope()
  152. scope.run(() => {
  153. onScopeDispose(spy)
  154. })
  155. expect(spy).toHaveBeenCalledTimes(0)
  156. onScopeDispose(spy)
  157. expect(
  158. '[Vue warn] onScopeDispose() is called when there is no active effect scope to be associated with.'
  159. ).toHaveBeenWarned()
  160. scope.stop()
  161. expect(spy).toHaveBeenCalledTimes(1)
  162. })
  163. it('should derefence child scope from parent scope after stopping child scope (no memleaks)', () => {
  164. const parent = new EffectScope()
  165. const child = parent.run(() => new EffectScope())!
  166. expect(parent.scopes!.includes(child)).toBe(true)
  167. child.stop()
  168. expect(parent.scopes!.includes(child)).toBe(false)
  169. })
  170. it('test with higher level APIs', async () => {
  171. const r = ref(1)
  172. const computedSpy = jest.fn()
  173. const watchSpy = jest.fn()
  174. const watchEffectSpy = jest.fn()
  175. let c: ComputedRef
  176. const scope = new EffectScope()
  177. scope.run(() => {
  178. c = computed(() => {
  179. computedSpy()
  180. return r.value + 1
  181. })
  182. watch(r, watchSpy)
  183. watchEffect(() => {
  184. watchEffectSpy()
  185. r.value
  186. })
  187. })
  188. c!.value // computed is lazy so trigger collection
  189. expect(computedSpy).toHaveBeenCalledTimes(1)
  190. expect(watchSpy).toHaveBeenCalledTimes(0)
  191. expect(watchEffectSpy).toHaveBeenCalledTimes(1)
  192. r.value++
  193. c!.value
  194. await nextTick()
  195. expect(computedSpy).toHaveBeenCalledTimes(2)
  196. expect(watchSpy).toHaveBeenCalledTimes(1)
  197. expect(watchEffectSpy).toHaveBeenCalledTimes(2)
  198. scope.stop()
  199. r.value++
  200. c!.value
  201. await nextTick()
  202. // should not trigger anymore
  203. expect(computedSpy).toHaveBeenCalledTimes(2)
  204. expect(watchSpy).toHaveBeenCalledTimes(1)
  205. expect(watchEffectSpy).toHaveBeenCalledTimes(2)
  206. })
  207. })