import { OperationTypes } from './operations' import { Dep, targetMap } from './reactive' import { EMPTY_OBJ, extend } from '@vue/shared' export interface ReactiveEffect { (): T _isEffect: true active: boolean raw: () => T deps: Array options: ReactiveEffectOptions } export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean scheduler?: (run: Function) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void } export type DebuggerEvent = { effect: ReactiveEffect target: object type: OperationTypes key: any } & DebuggerEventExtraInfo export interface DebuggerEventExtraInfo { newValue?: any oldValue?: any oldTarget?: Map | Set } export const effectStack: ReactiveEffect[] = [] export const ITERATE_KEY = Symbol('iterate') export function isEffect(fn: any): fn is ReactiveEffect { return fn != null && fn._isEffect === true } export function effect( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect { if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) if (!options.lazy) { effect() } return effect } export function stop(effect: ReactiveEffect) { if (effect.active) { cleanup(effect) if (effect.options.onStop) { effect.options.onStop() } effect.active = false } } function createReactiveEffect( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function reactiveEffect(...args: unknown[]): unknown { return run(effect, fn, args) } as ReactiveEffect effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown { if (!effect.active) { return fn(...args) } if (!effectStack.includes(effect)) { cleanup(effect) try { effectStack.push(effect) return fn(...args) } finally { effectStack.pop() } } } function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } } let shouldTrack = true export function pauseTracking() { shouldTrack = false } export function resumeTracking() { shouldTrack = true } export function track(target: object, type: OperationTypes, key?: unknown) { if (!shouldTrack || effectStack.length === 0) { return } const effect = effectStack[effectStack.length - 1] if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.options.onTrack) { effect.options.onTrack({ effect, target, type, key }) } } } export function trigger( target: object, type: OperationTypes, key?: unknown, extraInfo?: DebuggerEventExtraInfo ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } const effects = new Set() const computedRunners = new Set() if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. computedRunners.forEach(run) effects.forEach(run) } function addRunners( effects: Set, computedRunners: Set, effectsToAdd: Set | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } } function scheduleRun( effect: ReactiveEffect, target: object, type: OperationTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) { if (__DEV__ && effect.options.onTrigger) { const event: DebuggerEvent = { effect, target, key, type } effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event) } if (effect.options.scheduler !== void 0) { effect.options.scheduler(effect) } else { effect() } }