Skip to main content
Module

x/vue3_reactivity/src/effect.ts

vue3_reactivity is forck by @vue/reactivity for deno.
Latest
File
// deno-lint-ignore-file no-explicit-any ban-types prefer-constimport { TrackOpTypes, TriggerOpTypes } from './operations.ts'import { extend, isArray, isIntegerKey, isMap } from '../shared/index.ts'import { EffectScope, recordEffectScope } from './effectScope.ts'import { createDep, Dep, finalizeDepMarkers, initDepMarkers, newTracked, wasTracked} from './dep.ts'import { ComputedRefImpl } from './computed.ts'import { __DEV__ } from "../dev.ts";
// 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<any, Dep>const targetMap = new WeakMap<any, KeyToDepMap>()
// The number of effects currently being tracked recursively.let effectTrackDepth = 0
export let trackOpBit = 1
/** * The bitwise track markers support at most 30 levels of 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 = (...args: any[]) => any
export type DebuggerEvent = { effect: ReactiveEffect} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = { target: object type: TrackOpTypes | TriggerOpTypes key: any newValue?: any oldValue?: any oldTarget?: Map<any, any> | Set<any>}
export 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<T = any> { active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined
/** * Can be attached after creation * @internal */ computed?: ComputedRefImpl<T> /** * @internal */ allowRecurse?: boolean /** * @internal */ private deferStop?: boolean
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 ) { recordEffectScope(this, scope) }
run() { if (!this.active) { return this.fn() } let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack while (parent) { if (parent === this) { return } parent = parent.parent } try { this.parent = activeEffect activeEffect = this shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { cleanupEffect(this) } return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) }
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent shouldTrack = lastShouldTrack this.parent = undefined
if (this.deferStop) { this.stop() } } }
stop() { // stopped while running itself - defer the cleanup if (activeEffect === this) { this.deferStop = true } else 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 DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void}
export interface ReactiveEffectOptions extends DebuggerOptions { lazy?: boolean scheduler?: EffectScheduler scope?: EffectScope allowRecurse?: boolean onStop?: () => void}
export interface ReactiveEffectRunner<T = any> { (): T effect: ReactiveEffect}
export function effect<T = any>( 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()}
export let shouldTrack = trueconst 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) { 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 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({ effect: activeEffect!, ...debuggerEventExtraInfo! }) } }}
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown>) { 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]) { if (__DEV__) { triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } else { const effects: ReactiveEffect[] = [] for (const dep of deps) { if (dep) { effects.push(...dep) } } if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } }}
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo) { // spread into array for stabilization const effects = isArray(dep) ? dep : [...dep] for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } } for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo) } }}
function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { effect.run() } }}