import { TrackOpTypes, TriggerOpTypes } from './operations' import { EMPTY_OBJ, 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 { (...args: any[]): T _isEffect: true id: number active: boolean raw: () => T deps: Array options: ReactiveEffectOptions } export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean scheduler?: (job: ReactiveEffect) => 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(__DEV__ ? 'iterate' : '') export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '') export function isEffect(fn: any): fn is ReactiveEffect { return fn && 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 } } let uid = 0 function createReactiveEffect( fn: (...args: any[]) => T, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function reactiveEffect(...args: unknown[]): unknown { if (!effect.active) { return options.scheduler ? undefined : 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] } } } as ReactiveEffect effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } 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) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { 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) { // never been tracked return } const effects = new Set() const computedRunners = new Set() const add = (effectsToAdd: Set | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || !shouldTrack) { 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 } }) } } if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { add(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET const isAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && !isArray(target)) if ( isAddOrDelete || (type === TriggerOpTypes.SET && target instanceof Map) ) { add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) } if (isAddOrDelete && target instanceof Map) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type, newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } // 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) }