import { OperationTypes } from './operations' import { Dep, targetMap } from './state' export interface ReactiveEffect { (): any isEffect: true active: boolean raw: Function deps: Array computed?: boolean scheduler?: Scheduler onTrack?: Debugger onTrigger?: Debugger } export interface ReactiveEffectOptions { lazy?: boolean computed?: boolean scheduler?: Scheduler onTrack?: Debugger onTrigger?: Debugger } export type Scheduler = (run: () => any) => void export type DebuggerEvent = { effect: ReactiveEffect target: any type: OperationTypes key: string | symbol | undefined } export type Debugger = (event: DebuggerEvent) => void export const activeReactiveEffectStack: ReactiveEffect[] = [] export const ITERATE_KEY = Symbol('iterate') export function createReactiveEffect( fn: Function, options: ReactiveEffectOptions ): ReactiveEffect { const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect effect.isEffect = true effect.active = true effect.raw = fn effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.computed = options.computed effect.deps = [] return effect } function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } } export function cleanup(effect: ReactiveEffect) { for (let i = 0; i < effect.deps.length; i++) { effect.deps[i].delete(effect) } effect.deps.length = 0 } export function track( target: any, type: OperationTypes, key?: string | symbol ) { const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { 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 as string | symbol) if (!dep) { depsMap.set(key as string | symbol, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.onTrack) { effect.onTrack({ effect, target, type, key }) } } } } export function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { 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 as string | symbol)) } // 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.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } } function scheduleRun( effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( Object.assign( { effect, target, key, type }, extraInfo ) ) } if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }