Open-Source Wikis

/

Vue.js

/

Packages

/

reactivity

vuejs/core

reactivity

Active contributors: Evan You, daiwei, Carlos Rodrigues

Purpose

@vue/reactivity is the framework-agnostic implementation of Vue's reactivity system. It provides the public primitives ref, reactive, computed, effect, watch, and effectScope, plus the lower-level track/trigger plumbing that all of them are built on. The package can be used standalone (Pinia, VueUse, and a long list of third-party state libraries do this); inside Vue it is the foundation that runtime-core builds the component update model on.

Directory layout

packages/reactivity/src/
├── index.ts                    # public API barrel
├── constants.ts                # ReactiveFlags, TrackOpTypes, TriggerOpTypes
├── ref.ts                      # ref, shallowRef, RefImpl, customRef, toRef, toRefs, proxyRefs
├── reactive.ts                 # reactive, readonly, shallowReactive, shallowReadonly, markRaw, toRaw
├── computed.ts                 # computed + ComputedRefImpl
├── effect.ts                   # ReactiveEffect, EffectFlags, public effect()/stop()
├── effectScope.ts              # EffectScope + scope-aware cleanup
├── dep.ts                      # Dep, doubly-linked Link list, track/trigger, ITERATE_KEY, …
├── watch.ts                    # watch, traverse, getCurrentWatcher, onWatcherCleanup
├── baseHandlers.ts             # Proxy traps for Object/Array
├── collectionHandlers.ts       # Proxy traps for Map/Set/WeakMap/WeakSet
├── arrayInstrumentations.ts    # patched Array methods (includes, indexOf, push, pop, …)
└── warning.ts                  # tiny warn() shim

Core data model

A reactive system needs to answer two questions: what was read and what should re-run when it changes. Vue 3 answers both using a directional doubly-linked list:

  • A Dep (packages/reactivity/src/dep.ts) represents one tracked location: a property of a reactive object, the value of a ref, or a synthetic key like ITERATE_KEY (Map/Set iteration), MAP_KEY_ITERATE_KEY, or ARRAY_ITERATE_KEY.
  • A Subscriber (packages/reactivity/src/effect.ts) is anything that wants to be notified: a ReactiveEffect, a ComputedRefImpl, or a watcher. Each subscriber owns a list of Links — one per dep it currently depends on.
  • A Link is a node that lives in two lists: the dep's list of subscribers, and the subscriber's list of deps. This is what makes incremental dependency change-tracking cheap; on each effect run, links are recycled and stale ones are unlinked in a single pass.

The EffectFlags enum (ACTIVE | RUNNING | TRACKING | NOTIFIED | DIRTY | ALLOW_RECURSE | PAUSED | EVALUATED) carries per-effect state in a single integer. globalVersion in dep.ts is bumped on every trigger and used by computeds to skip recomputation when no dep has changed.

graph LR
    subgraph "Reactive object"
      depX["Dep for x"]
      depY["Dep for y"]
    end

    subgraph "Subscribers"
      effA["ReactiveEffect A<br/>(component update)"]
      compB["ComputedRefImpl<br/>(some computed)"]
    end

    depX -- Link --> effA
    depX -- Link --> compB
    depY -- Link --> compB

Public API

The package re-exports through packages/reactivity/src/index.ts. The entry points users call most often:

| API | File | Description | | --------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | | ref(v) / shallowRef(v) | packages/reactivity/src/ref.ts | Wraps a value in RefImpl whose .value getter calls track() and setter calls trigger(). | | reactive(o) / readonly(o) / shallowReactive(o) / shallowReadonly(o) | packages/reactivity/src/reactive.ts | Wraps an object/array/Map/Set in a Proxy. Cached in per-target WeakMaps so reactive(x) === reactive(x). | | computed(getter) / computed({ get, set }) | packages/reactivity/src/computed.ts | Creates a ComputedRefImpl that lazily evaluates and caches. Marked dirty by globalVersion mismatch + dep dirty checks. | | effect(fn, options?) | packages/reactivity/src/effect.ts | Runs fn immediately and again whenever any tracked dep changes. Returns a runner with attached effect. | | watch(source, cb, options?) | packages/reactivity/src/watch.ts | High-level wrapper. Sources can be refs, getters, reactives, or arrays of those. Supports immediate, deep, flush: 'pre' | 'post' | 'sync'. | | effectScope([detached]) | packages/reactivity/src/effectScope.ts | Owns a list of effects and disposes them as a group. The component instance is itself an EffectScope. | | markRaw(o) / toRaw(o) | packages/reactivity/src/reactive.ts | Opt out of reactivity / unwrap a proxy. | | track(target, type, key) / trigger(target, type, key, …) | packages/reactivity/src/dep.ts | Low-level tracking primitives, exposed for integration libraries. | | pauseTracking() / enableTracking() / resetTracking() | packages/reactivity/src/effect.ts | Stack-based pause/resume for the active subscriber. Used by the renderer when running compiled render functions in a non-tracking context. |

How tracking works

sequenceDiagram
    participant User
    participant Proxy as reactive() Proxy
    participant Dep
    participant Sub as Active subscriber
    User->>Proxy: read state.x
    Proxy->>Dep: track(state, 'get', 'x')
    Dep->>Sub: link in/out of Sub's deps list
    User->>Proxy: state.x = 1
    Proxy->>Dep: trigger(state, 'set', 'x', 1)
    Dep->>Sub: notify() (queue or run)

The proxy traps live in two files:

  • packages/reactivity/src/baseHandlers.ts handles Object and Array reads/writes. Plain reads call track; sets call trigger and bump globalVersion. Read-modify-write methods on arrays are special-cased through arrayInstrumentations.ts so iteration triggers the right key.
  • packages/reactivity/src/collectionHandlers.ts handles Map/Set/WeakMap/WeakSet. Iteration uses ITERATE_KEY (MAP_KEY_ITERATE_KEY for Map.keys()) to track the whole collection.

Refs live outside the proxy machinery: RefImpl.get value() calls track(this, …) on a per-ref dep, and set value(v) writes the new value, calls trigger, and re-queues subscribers.

Effects, scopes, and pausing

ReactiveEffect is the single class behind effect(), watchEffect, the renderer's component update job, and the auxiliary effect spawned by every computed. The class:

  1. Records its dependencies during run() by setting itself as activeSub (packages/reactivity/src/effect.ts).
  2. Detaches stale links after each run.
  3. Notifies dirty when triggered. By default it runs synchronously; the user provides a scheduler to defer (e.g., the renderer.ts scheduler that defers to a microtask via queueJob).

Effects belong to the activeEffectScope (or are detached). Disposing the scope disposes every effect in one pass — this is how Vue cleans up a component on unmount and how Pinia cleans up its stores.

pauseTracking() and enableTracking() push/pop the activeSub. They are used by runtime-core when running compiled render functions (which should not retrack on each render) and inside transitions to suppress unwanted dependencies.

Computed

ComputedRefImpl is both a Subscriber (it has a ReactiveEffect underneath) and a Dep (it can be subscribed to). When the inner effect is triggered, the computed marks itself DIRTY and notifies its own dep. The next read recomputes; until then value returns the cached result. The globalVersion short-circuit lets a computed bail out without walking its deps when the system has not changed at all since the last evaluation.

Watch

watch() (packages/reactivity/src/watch.ts) accepts:

  • A function (the source) to invoke and track.
  • A callback to run after the source changes.
  • Options: immediate, once, deep, flush: 'pre' | 'post' | 'sync', onTrack, onTrigger.

It builds a ReactiveEffect whose body calls the source, snapshots the new value, and queues the callback through the supplied scheduler. The flush option determines which scheduler to use: pre/post route through runtime-core/scheduler.ts; sync runs immediately.

onWatcherCleanup(fn) registers a cleanup for the current watch run, called before the next run or when the scope disposes — the standard pattern for cancelling pending requests.

Caveats

The README calls these out:

  • Built-in object types other than Array, Map, WeakMap, Set, WeakSet are not tracked. Wrapping a Date, a class instance, or a typed array does not make it reactive.
  • The standalone build of @vue/reactivity and the inlined version inside vue.global.js should not be mixed. They keep separate dep maps; an object wrapped by one will not trigger effects defined in the other.

Integration points

  • Used by packages/runtime-core/src/component.ts to wrap the setup state in a proxyRefs proxy so refs auto-unwrap in templates.
  • The component update function is a ReactiveEffect whose scheduler is queueJob (packages/runtime-core/src/scheduler.ts).
  • apiWatch.ts in runtime-core re-exposes watch with the integration into the component lifecycle (cleanup on unmount, error handling).
  • Type augmentation: packages/runtime-core/src/index.ts augments RefUnwrapBailTypes so vnode and component instances are not deeply unwrapped by UnwrapRef.

Entry points for modification

  • New tracked operation? Add a TrackOpTypes/TriggerOpTypes constant in constants.ts, then call track/trigger from the right proxy trap.
  • New ref-like primitive? Subclass the pattern in ref.ts (read calls track, write calls trigger).
  • Performance work? The hot paths are Dep.track, Dep.trigger, and ReactiveEffect.run. The benchmark suite under packages/reactivity/__benchmarks__/ exists for exactly this; run with pnpm bench.

For how reactivity hooks into rendering, see features/reactivity-system and packages/runtime-core.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

reactivity – Vue.js wiki | Factory