import type { ReactiveEffect } from './effect' import { warn } from './warning' export let activeEffectScope: EffectScope | undefined export class EffectScope { /** * @internal */ private _active = true /** * @internal track `on` calls, allow `on` call multiple times */ private _on = 0 /** * @internal */ effects: ReactiveEffect[] = [] /** * @internal */ cleanups: (() => void)[] = [] private _isPaused = false /** * only assigned by undetached scope * @internal */ parent: EffectScope | undefined /** * record undetached scopes * @internal */ scopes: EffectScope[] | undefined /** * track a child scope's index in its parent's scopes array for optimized * removal * @internal */ private index: number | undefined constructor(public detached = false) { this.parent = activeEffectScope if (!detached && activeEffectScope) { this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( this, ) - 1 } } get active(): boolean { return this._active } pause(): void { if (this._active) { this._isPaused = true let i, l if (this.scopes) { for (i = 0, l = this.scopes.length; i < l; i++) { this.scopes[i].pause() } } for (i = 0, l = this.effects.length; i < l; i++) { this.effects[i].pause() } } } /** * Resumes the effect scope, including all child scopes and effects. */ resume(): void { if (this._active) { if (this._isPaused) { this._isPaused = false let i, l if (this.scopes) { for (i = 0, l = this.scopes.length; i < l; i++) { this.scopes[i].resume() } } for (i = 0, l = this.effects.length; i < l; i++) { this.effects[i].resume() } } } } run(fn: () => T): T | undefined { if (this._active) { const currentEffectScope = activeEffectScope try { activeEffectScope = this return fn() } finally { activeEffectScope = currentEffectScope } } else if (__DEV__) { warn(`cannot run an inactive effect scope.`) } } prevScope: EffectScope | undefined /** * This should only be called on non-detached scopes * @internal */ on(): void { if (++this._on === 1) { this.prevScope = activeEffectScope activeEffectScope = this } } /** * This should only be called on non-detached scopes * @internal */ off(): void { if (this._on > 0 && --this._on === 0) { activeEffectScope = this.prevScope this.prevScope = undefined } } stop(fromParent?: boolean): void { if (this._active) { this._active = false let i, l for (i = 0, l = this.effects.length; i < l; i++) { this.effects[i].stop() } this.effects.length = 0 for (i = 0, l = this.cleanups.length; i < l; i++) { this.cleanups[i]() } this.cleanups.length = 0 if (this.scopes) { for (i = 0, l = this.scopes.length; i < l; i++) { this.scopes[i].stop(true) } this.scopes.length = 0 } // nested scope, dereference from parent to avoid memory leaks if (!this.detached && this.parent && !fromParent) { // optimized O(1) removal const last = this.parent.scopes!.pop() if (last && last !== this) { this.parent.scopes![this.index!] = last last.index = this.index! } } this.parent = undefined } } } /** * Creates an effect scope object which can capture the reactive effects (i.e. * computed and watchers) created within it so that these effects can be * disposed together. For detailed use cases of this API, please consult its * corresponding {@link https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md | RFC}. * * @param detached - Can be used to create a "detached" effect scope. * @see {@link https://vuejs.org/api/reactivity-advanced.html#effectscope} */ export function effectScope(detached?: boolean): EffectScope { return new EffectScope(detached) } /** * Returns the current active effect scope if there is one. * * @see {@link https://vuejs.org/api/reactivity-advanced.html#getcurrentscope} */ export function getCurrentScope(): EffectScope | undefined { return activeEffectScope } /** * Registers a dispose callback on the current active effect scope. The * callback will be invoked when the associated effect scope is stopped. * * @param fn - The callback function to attach to the scope's cleanup. * @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose} */ export function onScopeDispose(fn: () => void, failSilently = false): void { if (activeEffectScope) { activeEffectScope.cleanups.push(fn) } else if (__DEV__ && !failSilently) { warn( `onScopeDispose() is called when there is no active effect scope` + ` to be associated with.`, ) } }