import { TrackOpTypes, TriggerOpTypes } from './operations' import { EMPTY_OBJ, isArray, isIntegerKey, isMap } 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 id: number active: boolean raw: () => T deps: Array options: ReactiveEffectOptions allowRecurse: boolean } export interface ReactiveEffectOptions { lazy?: boolean scheduler?: (job: ReactiveEffect) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void /** * Indicates whether the job is allowed to recursively trigger itself when * managed by the scheduler. * * By default, a job cannot trigger itself because some built-in method calls, * e.g. Array.prototype.push actually performs reads as well (#1740) which * can lead to confusing infinite loops. * The allowed cases are component update functions and watch callbacks. * Component update functions may update child component props, which in turn * trigger flush: "pre" watch callbacks that mutates state that the parent * relies on (#1801). Watch callbacks doesn't track its dependencies so if it * triggers itself again, it's likely intentional and it is the user's * responsibility to perform recursive state mutation that eventually * stabilizes (#1727). */ allowRecurse?: boolean } 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[] = [] 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: () => T, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function reactiveEffect(): unknown { if (!effect.active) { return fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn() } finally { effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse 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 add = (effectsToAdd: Set | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect) } }) } } 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 switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes add(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { add(depsMap.get(ITERATE_KEY)) if (isMap(target)) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { add(depsMap.get(ITERATE_KEY)) } break } } 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() } } effects.forEach(run) }