4. Reactivity

Vue Reactivity: Dependency Tracking via Proxy get/set

seniorfrontend-internalsreactivity

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 of self._running_effect_stack), register it as a dependent of (target, key): add it to self._deps[(id(target), key)] (creating that set if needed) AND add that same set to the effect's own dep_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 up self._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 (an if/else inside 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 in self.dep_sets, then clear dep_sets) -- otherwise a property it no longer reads could still incorrectly retrigger it. Then push this effect onto self.system._running_effect_stack, call self.fn(), and pop it back off (even if fn() 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 any effect() call).
  • Effect.run()'s cleanup must happen BEFORE fn() 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 (use try/finally) -- otherwise one crashing effect would corrupt tracking for every future track() call.

Real-world grounding

  • Vue core's dependency tracking and effect runner: packages/reactivity/src/effect.ts -- see track/trigger and ReactiveEffect.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.

hints
vue_reactivity.pysign in to run
Loading...
test result

This is a preview. Sign in to run this exercise against the real tests.