Vue's reactivity system intercepts property reads and writes with Proxy traps and ref accessors, records which effect read which property, and re-runs exactly those effects when the property changes.
In Vue you mutate state directly: state.count++ or count.value++. There is no setter function to call and no dependency array to declare. The framework still knows what to update because every reactive object is a Proxy and every ref is an object with an accessor, so each read and write passes through code Vue controls.
That interception feeds two operations. During an effect's run, the get trap calls track(target, key), which adds the currently running effect to a Dep set stored under that target and key in a global WeakMap called targetMap. When a write reaches the set trap, trigger(target, key) looks up the same Dep and schedules every effect in it. The set of dependencies is rebuilt on every run, so a branch that stops reading a property also stops subscribing to it.
Effects come in a few forms with the same core. watchEffect and watch wrap a user callback. computed wraps a getter, caches the result, and flips a dirty flag on trigger instead of recomputing eagerly. A component's render function is also an effect, which is why a Vue component re-renders only when a property it actually read changes, rather than on every state update it owns as in React.
Triggered effects do not run inline by default. A scheduler pushes them into a job queue, deduplicates by identity, and flushes the queue in a single microtask. Several synchronous writes therefore produce one re-run. nextTick returns the promise that resolves after that flush. Vue 3.4 rewrote the internals around version counters so a computed can check whether any dependency changed without re-walking its subscriptions, and 3.5 lowered memory use further, but the track and trigger model is unchanged.
Interview framing: define Vue Reactivity in one sentence, then explain one concrete runtime behavior and one common pitfall with a short code example.