import { TrackOpTypes, TriggerOpTypes } from './operations' import { EMPTY_OBJ, extend, isArray } from '@vue/shared' // The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class // which maintains a Set of subscribers, but we simply store them as // raw Sets to reduce memory overhead. type Dep = Set type KeyToDepMap = Map const targetMap = new WeakMap() 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: TrackOpTypes | TriggerOpTypes key: any } & DebuggerEventExtraInfo export interface DebuggerEventExtraInfo { newValue?: any oldValue?: any oldTarget?: Map | Set } const effectStack: ReactiveEffect[] = [] export let activeEffect: ReactiveEffect | undefined 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 { enableTracking() effectStack.push(effect) activeEffect = effect return fn(...args) } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } 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 const trackStack: boolean[] = [] export function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false } export function enableTracking() { trackStack.push(shouldTrack) shouldTrack = true } export function resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last } export function track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return } 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(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } } export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // never been tracked return } const effects = new Set() const computedRunners = new Set() if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { 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 | Map.SET if ( type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE || (type === TriggerOpTypes.SET && target instanceof Map) ) { const iterationKey = isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } const run = (effect: ReactiveEffect) => { scheduleRun( effect, target, type, key, __DEV__ ? { newValue, oldValue, oldTarget } : undefined ) } // 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 !== activeEffect) { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } } else { // the effect mutated its own dependency during its execution. // this can be caused by operations like foo.value++ // do not trigger or we end in an infinite loop } }) } } function scheduleRun( effect: ReactiveEffect, target: object, type: TriggerOpTypes, 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() } }