skip to content

In Vue 3, how does the reactivity system know exactly which effects to re-run when one property of a `reactive()` object changes?

level: middleimportance: must knowfreq 58%

answer

  1. get trap and set trap
  2. WeakMap, then Map, then dep
  3. the active effect gets linked
  4. no trigger when value unchanged

basics

~20 s

The proxy's get trap calls track(target, key), linking the running effect to a dep stored under targetMap, then target, then key. The set trap calls trigger(target, key) only when the value changed or the key is new, notifying just that key's subscribers.

solid answer

~40 s

Every `reactive()` object is a Proxy. Its `get` trap calls `track(target, key)`: if an effect is running, Vue looks up `targetMap` (a `WeakMap` from raw object to a `Map` of key to `Dep`), creates the dep on first use, and links it to the active effect. Its `set` trap calls `trigger(target, key)` only when the key is new or `Object.is` says the value changed; `trigger` walks that one dep's subscribers and notifies them - component render effects are queued, computeds are marked dirty. Adding or deleting a key also notifies an iteration dep, so effects that ran `Object.keys` or a `for...in` re-run. Refs skip `targetMap`: each ref owns its dep directly.

code

ts · 13 lines
ts
import { reactive, watchEffect } from 'vue'

const state = reactive({ a: 1, b: 1 })
let runs = 0

watchEffect(() => {
  runs++
  void state.a // tracked: links this effect to the dep for key 'a'
})

state.b++    // no re-run scheduled: the effect never read 'b'
state.a = 1  // no re-run scheduled: Object.is(1, 1), set trap skips trigger
state.a = 2  // re-run scheduled: 'a' changed

go deeper

for a junior

Recall that reading a reactive property subscribes the running render and writing it notifies only the renders that read it.

for a middle

Walk the chain: get trap to track, targetMap to key to dep, set trap to trigger, with no trigger for an unchanged value.

for a senior

Use the per-key model to explain surprising updates or non-updates, such as Object.keys consumers re-running on add but not on value changes.

for a principal

Weigh runtime tracking's memory cost per tracked key against its precision, and when shallow or raw data is the better default for large structures.

## The two halves: `track()` and `trigger()` Vue 3's reactivity rests on two functions in `packages/reactivity/src/dep.ts`: - **`track(target, type, key)`** - called on a read; records "the effect running right now depends on `target[key]`". - **`trigger(target, type, key, ...)`** - called on a write; notifies every effect recorded for `target[key]`. A **reactive effect** is any function Vue runs while watching its reads: a component's render effect, a `computed` getter, a `watchEffect` function or a `watch` source getter. Vue keeps a module-level pointer to the one currently running, the **active subscriber** (`activeSub` in the source), and a `shouldTrack` switch. ## Where the calls come from `reactive(obj)` returns a `Proxy` over the raw object. In its handler: 1. The **`get` trap** reads the value with `Reflect.get`, then calls `track(target, 'get', key)` - unless the proxy is `readonly`, which never tracks because it can never change. If the value is itself an object, it is wrapped lazily in a reactive proxy on the way out. 2. The **`set` trap** writes with `Reflect.set`, then calls `trigger` with type `add` if the key did not exist, or type `set` if it existed **and** `Object.is(newValue, oldValue)` is false. Assigning the same value triggers nothing. 3. The **`has`**, **`ownKeys`** and **`deleteProperty`** traps track or trigger too, so `key in state`, `Object.keys(state)` and `delete state.key` take part. A `ref` does not go through a proxy: its `.value` getter calls `dep.track()` on the ref's own `Dep`, and its setter calls `dep.trigger()`. ## The storage: `targetMap` -> key -> dep | Level | Structure | Key | Holds | |---|---|---|---| | 1 | `targetMap: WeakMap` | the raw object | a `Map` for that object | | 2 | `Map` | property key (or an internal iteration symbol) | one `Dep` | | 3 | `Dep` | - | its subscribers and a `version` counter | `track` creates the inner `Map` and the `Dep` on first use, and only when an effect is active - reading a reactive object in a click handler allocates nothing. Using a `WeakMap` means that when the application drops the raw object, its whole dependency record can be garbage-collected without any cleanup call. Because deps are per key, an effect that read `state.a` is linked only to `a`'s dep. Writing `state.b` never reaches it. That is the precision behind the claim that Vue updates "exactly" what depends on a change. ## What changed in 3.5: links and versions Vue 3.5 refactored this layer to **version counting with doubly-linked lists**. Every dependency relationship is a `Link` node that sits in two lists at once: the dep's list of subscribers and the effect's list of deps. Each `Dep` has a `version` incremented on every trigger, and each link remembers the version it last saw. A global counter, `globalVersion`, is bumped on every reactive change. - Before an effect re-runs, all its links are marked unused; reads during the run re-mark them; links left unused are removed afterwards. Dependencies therefore always match the last run. - A scheduled component update checks, before re-rendering, whether any linked dep's version actually moved - which lets a computed that re-evaluated to the same result stop the update. The public shape - proxy traps calling `track` and `trigger`, deps per target and key - is the same one Vue 3 has had since 3.0. ## Iteration and structural changes Some reads depend on the **set of keys**, not a single key. `ownKeys` tracks an internal `ITERATE_KEY` (or `length` for arrays). `trigger` with type `add` or `delete` also runs that iteration dep; a plain `set` on an existing key of an ordinary object does not, because the key list did not change. Arrays and `Map`/`Set` have extra rules in the same function (for example, shrinking `length` notifies deps for the removed indices). ## Common misconceptions - "Vue re-renders everything that uses the object." Subscriptions are per key. - "Assigning triggers." Only a changed value or a new key triggers. - "Dependencies are found at compile time." They are collected at runtime, on each run of the effect. In an interview, walk the chain aloud: proxy trap -> `track`/`trigger` -> `targetMap` -> key -> dep -> subscribed effects.

  • Why is Vue's `targetMap` a WeakMap rather than a Map?
    Its keys are raw objects. A WeakMap does not keep its keys alive, so when the application releases an object, the object and its per-key deps become collectable without Vue having to unregister anything. A Map would leak every object that was ever made reactive and read inside an effect.
  • What did Vue 3.5 change about how deps store their subscribers?
    3.5 replaced the per-dep collections with `Link` nodes in two doubly-linked lists (dep to subscribers, effect to deps) plus version counters on each dep and a global version. Stale links are marked before each run and pruned after it, and version checks let effects skip work when nothing they read actually changed.
  • Does reading a reactive property when no effect is running cost anything?
    Only the proxy trap itself. `track()` returns immediately when there is no active effect or tracking is paused, so no inner Map, dep or link is created for reads made in event handlers or plain functions.

Each property keeps its own sign-up sheet. Any effect that reads the property while running signs that sheet; a write to the property phones only the names on its sheet, and phones nobody if the value did not really change.

saying these in an interview costs you the question

  • Vue re-renders every component whenever any reactive object changes.
  • Vue diffs the old and new object to find which properties changed.
  • Assigning a reactive property the value it already has still triggers effects.
  • Dependencies are worked out at compile time from the template's identifiers.
  • Each reactive object keeps one subscriber list shared by all of its keys.