React's virtual DOM answers "what changed?" by re-rendering a whole component and diffing the result. Vue 3 takes a different approach: it tracks, PER REACTIVE PROPERTY, exactly which "effects" (computed values, watchers, a component's render function) read that property the last time they ran -- so when the property changes, only THOSE effects need to re-run, nothing else. No re-render, no diff, no virtual DOM needed for this part at all.
A real Proxy isn't needed to model the mechanism -- plain get()/set()
methods that call track()/trigger() do the exact same job a Proxy's
get/set traps would.
Implement:
ReactiveSystem.track(target, key)-- called from inside a property read. If an effect is currently running (top ofself._running_effect_stack), register it as a dependent of(target, key): add it toself._deps[(id(target), key)](creating that set if needed) AND add that same set to the effect's owndep_sets(so the effect can find and clean up its subscriptions later). If no effect is running, do nothing.ReactiveSystem.trigger(target, key)-- called from inside a property write. Look upself._deps[(id(target), key)]; if it exists and is non-empty, call.run()on every effect registered there.Effect.run()-- an effect's dependencies can change between runs (anif/elseinside it might read a different property depending on some flag), so every run must first REMOVE this effect from every dep set it was registered in during its PREVIOUS run (dep_set.discard (self)for each set inself.dep_sets, then cleardep_sets) -- otherwise a property it no longer reads could still incorrectly retrigger it. Then push this effect ontoself.system._running_effect_stack, callself.fn(), and pop it back off (even iffn()raises).
ReactiveSystem.effect(fn) and the Reactive get/set wrapper are given.
Example
system = ReactiveSystem()
state = Reactive(system, count=0)
log = []
system.effect(lambda: log.append(state.get("count"))) # runs immediately -> log == [0]
state.set("count", 1) # triggers the effect
log # -> [0, 1]
Constraints
track()must be a no-op when no effect is currently running (e.g. a plain, non-tracked property read outside anyeffect()call).Effect.run()'s cleanup must happen BEFOREfn()runs, not after -- otherwise fresh subscriptions from this run would be wiped out along with the stale ones.- The running-effect stack must be popped even if
fn()raises (usetry/finally) -- otherwise one crashing effect would corrupt tracking for every futuretrack()call.
Real-world grounding
- Vue core's dependency tracking and effect runner:
packages/reactivity/src/effect.ts-- seetrack/triggerandReactiveEffect.run()'s cleanup-before-rerun behavior, which this exercise implements a simplified version of.
Real Vue's Proxy get/set traps call track/trigger automatically
on every property access (no manual .get()/.set() calls needed) and
additionally distinguish several trigger "types" (add vs. set vs. delete,
which matters for arrays and Map/Set) -- this exercise implements the
core subscribe/cleanup/rerun mechanism underneath all of that.