effectScope.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import type { ReactiveEffect } from './effect'
  2. import { warn } from './warning'
  3. export let activeEffectScope: EffectScope | undefined
  4. export class EffectScope {
  5. /**
  6. * @internal
  7. */
  8. private _active = true
  9. /**
  10. * @internal track `on` calls, allow `on` call multiple times
  11. */
  12. private _on = 0
  13. /**
  14. * @internal
  15. */
  16. effects: ReactiveEffect[] = []
  17. /**
  18. * @internal
  19. */
  20. cleanups: (() => void)[] = []
  21. private _isPaused = false
  22. /**
  23. * only assigned by undetached scope
  24. * @internal
  25. */
  26. parent: EffectScope | undefined
  27. /**
  28. * record undetached scopes
  29. * @internal
  30. */
  31. scopes: EffectScope[] | undefined
  32. /**
  33. * track a child scope's index in its parent's scopes array for optimized
  34. * removal
  35. * @internal
  36. */
  37. private index: number | undefined
  38. constructor(public detached = false) {
  39. this.parent = activeEffectScope
  40. if (!detached && activeEffectScope) {
  41. this.index =
  42. (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
  43. this,
  44. ) - 1
  45. }
  46. }
  47. get active(): boolean {
  48. return this._active
  49. }
  50. pause(): void {
  51. if (this._active) {
  52. this._isPaused = true
  53. let i, l
  54. if (this.scopes) {
  55. for (i = 0, l = this.scopes.length; i < l; i++) {
  56. this.scopes[i].pause()
  57. }
  58. }
  59. for (i = 0, l = this.effects.length; i < l; i++) {
  60. this.effects[i].pause()
  61. }
  62. }
  63. }
  64. /**
  65. * Resumes the effect scope, including all child scopes and effects.
  66. */
  67. resume(): void {
  68. if (this._active) {
  69. if (this._isPaused) {
  70. this._isPaused = false
  71. let i, l
  72. if (this.scopes) {
  73. for (i = 0, l = this.scopes.length; i < l; i++) {
  74. this.scopes[i].resume()
  75. }
  76. }
  77. for (i = 0, l = this.effects.length; i < l; i++) {
  78. this.effects[i].resume()
  79. }
  80. }
  81. }
  82. }
  83. run<T>(fn: () => T): T | undefined {
  84. if (this._active) {
  85. const currentEffectScope = activeEffectScope
  86. try {
  87. activeEffectScope = this
  88. return fn()
  89. } finally {
  90. activeEffectScope = currentEffectScope
  91. }
  92. } else if (__DEV__) {
  93. warn(`cannot run an inactive effect scope.`)
  94. }
  95. }
  96. prevScope: EffectScope | undefined
  97. /**
  98. * This should only be called on non-detached scopes
  99. * @internal
  100. */
  101. on(): void {
  102. if (++this._on === 1) {
  103. this.prevScope = activeEffectScope
  104. activeEffectScope = this
  105. }
  106. }
  107. /**
  108. * This should only be called on non-detached scopes
  109. * @internal
  110. */
  111. off(): void {
  112. if (this._on > 0 && --this._on === 0) {
  113. activeEffectScope = this.prevScope
  114. this.prevScope = undefined
  115. }
  116. }
  117. stop(fromParent?: boolean): void {
  118. if (this._active) {
  119. this._active = false
  120. let i, l
  121. for (i = 0, l = this.effects.length; i < l; i++) {
  122. this.effects[i].stop()
  123. }
  124. this.effects.length = 0
  125. for (i = 0, l = this.cleanups.length; i < l; i++) {
  126. this.cleanups[i]()
  127. }
  128. this.cleanups.length = 0
  129. if (this.scopes) {
  130. for (i = 0, l = this.scopes.length; i < l; i++) {
  131. this.scopes[i].stop(true)
  132. }
  133. this.scopes.length = 0
  134. }
  135. // nested scope, dereference from parent to avoid memory leaks
  136. if (!this.detached && this.parent && !fromParent) {
  137. // optimized O(1) removal
  138. const last = this.parent.scopes!.pop()
  139. if (last && last !== this) {
  140. this.parent.scopes![this.index!] = last
  141. last.index = this.index!
  142. }
  143. }
  144. this.parent = undefined
  145. }
  146. }
  147. }
  148. /**
  149. * Creates an effect scope object which can capture the reactive effects (i.e.
  150. * computed and watchers) created within it so that these effects can be
  151. * disposed together. For detailed use cases of this API, please consult its
  152. * corresponding {@link https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md | RFC}.
  153. *
  154. * @param detached - Can be used to create a "detached" effect scope.
  155. * @see {@link https://vuejs.org/api/reactivity-advanced.html#effectscope}
  156. */
  157. export function effectScope(detached?: boolean): EffectScope {
  158. return new EffectScope(detached)
  159. }
  160. /**
  161. * Returns the current active effect scope if there is one.
  162. *
  163. * @see {@link https://vuejs.org/api/reactivity-advanced.html#getcurrentscope}
  164. */
  165. export function getCurrentScope(): EffectScope | undefined {
  166. return activeEffectScope
  167. }
  168. /**
  169. * Registers a dispose callback on the current active effect scope. The
  170. * callback will be invoked when the associated effect scope is stopped.
  171. *
  172. * @param fn - The callback function to attach to the scope's cleanup.
  173. * @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
  174. */
  175. export function onScopeDispose(fn: () => void, failSilently = false): void {
  176. if (activeEffectScope) {
  177. activeEffectScope.cleanups.push(fn)
  178. } else if (__DEV__ && !failSilently) {
  179. warn(
  180. `onScopeDispose() is called when there is no active effect scope` +
  181. ` to be associated with.`,
  182. )
  183. }
  184. }