import { TrackOpTypes, TriggerOpTypes } from './operations' import { extend, isArray, isIntegerKey, isMap } from '@vue/shared' import { EffectScope, recordEffectScope } from './effectScope' import { createDep, Dep, finalizeDepMarkers, initDepMarkers, newTracked, wasTracked } from './Dep' // 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 KeyToDepMap = Map const targetMap = new WeakMap() // The number of effects currently being tracked recursively. let effectTrackDepth = 0 export let trackOpBit = 1 /** * The bitwise track markers support at most 30 levels op recursion. * This value is chosen to enable modern JS engines to use a SMI on all platforms. * When recursion depth is greater, fall back to using a full cleanup. */ const maxMarkerBits = 30 export type EffectScheduler = () => void export type DebuggerEvent = { effect: ReactiveEffect } & DebuggerEventExtraInfo export type DebuggerEventExtraInfo = { target: object type: TrackOpTypes | TriggerOpTypes key: any 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 class ReactiveEffect { active = true deps: Dep[] = [] // can be attached after creation onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope | null, // allow recursive self-invocation public allowRecurse = false ) { recordEffectScope(this, scope) } run() { if (!this.active) { return this.fn() } if (!effectStack.includes(this)) { try { effectStack.push((activeEffect = this)) enableTracking() trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { cleanupEffect(this) } return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth resetTracking() effectStack.pop() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } } function cleanupEffect(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } } export interface ReactiveEffectOptions { lazy?: boolean scheduler?: EffectScheduler scope?: EffectScope allowRecurse?: boolean onStop?: () => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } export interface ReactiveEffectRunner { (): T effect: ReactiveEffect } export function effect( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } if (!options || !options.lazy) { _effect.run() } const runner = _effect.run.bind(_effect) as ReactiveEffectRunner runner.effect = _effect return runner } export function stop(runner: ReactiveEffectRunner) { runner.effect.stop() } 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 (!isTracking()) { 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 = createDep())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) } export function isTracking() { return shouldTrack && activeEffect !== undefined } export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit // set newly tracked shouldTrack = !wasTracked(dep) } } else { // Full cleanup mode. shouldTrack = !dep.has(activeEffect!) } if (shouldTrack) { dep.add(activeEffect!) activeEffect!.deps.push(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack( Object.assign( { effect: activeEffect! }, debuggerEventExtraInfo ) ) } } } 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 } let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target deps = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { deps.push(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes deps.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)) } break } } const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined if (deps.length === 1) { if (deps[0]) { triggerEffects(deps[0], eventInfo) } } else { const effects: ReactiveEffect[] = [] for (const dep of deps) { if (dep) { effects.push(...dep) } } triggerEffects(createDep(effects), eventInfo) } } export function triggerEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization for (const effect of [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { effect.run() } } } }