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() shimCore 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, thevalueof a ref, or a synthetic key likeITERATE_KEY(Map/Set iteration),MAP_KEY_ITERATE_KEY, orARRAY_ITERATE_KEY. - A
Subscriber(packages/reactivity/src/effect.ts) is anything that wants to be notified: aReactiveEffect, aComputedRefImpl, or a watcher. Each subscriber owns a list ofLinks — one per dep it currently depends on. - A
Linkis 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 --> compBPublic 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.tshandlesObjectandArrayreads/writes. Plain reads calltrack; sets calltriggerand bumpglobalVersion. Read-modify-write methods on arrays are special-cased througharrayInstrumentations.tsso iteration triggers the right key.packages/reactivity/src/collectionHandlers.tshandlesMap/Set/WeakMap/WeakSet. Iteration usesITERATE_KEY(MAP_KEY_ITERATE_KEYforMap.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:
- Records its dependencies during
run()by setting itself asactiveSub(packages/reactivity/src/effect.ts). - Detaches stale links after each run.
- Notifies dirty when triggered. By default it runs synchronously; the user provides a
schedulerto defer (e.g., therenderer.tsscheduler that defers to a microtask viaqueueJob).
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,WeakSetare not tracked. Wrapping aDate, a class instance, or a typed array does not make it reactive. - The standalone build of
@vue/reactivityand the inlined version insidevue.global.jsshould 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.tsto wrap the setup state in aproxyRefsproxy so refs auto-unwrap in templates. - The component update function is a
ReactiveEffectwhose scheduler isqueueJob(packages/runtime-core/src/scheduler.ts). apiWatch.tsin runtime-core re-exposeswatchwith the integration into the component lifecycle (cleanup on unmount, error handling).- Type augmentation:
packages/runtime-core/src/index.tsaugmentsRefUnwrapBailTypessovnodeand component instances are not deeply unwrapped byUnwrapRef.
Entry points for modification
- New tracked operation? Add a
TrackOpTypes/TriggerOpTypesconstant inconstants.ts, then calltrack/triggerfrom the right proxy trap. - New ref-like primitive? Subclass the pattern in
ref.ts(read callstrack, write callstrigger). - Performance work? The hot paths are
Dep.track,Dep.trigger, andReactiveEffect.run. The benchmark suite underpackages/reactivity/__benchmarks__/exists for exactly this; run withpnpm 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.