Vue but underthehood: Part 5 - Reactive Realities
Introduction
Welcome to the final installment of "Vue but underthehood"! We've journeyed through The Proxy Foundation, The Language Engine, The Safety Valve, and The Identity Crisis. Now, let's explore Reactive Realities - the complete picture of Vue's reactivity system.
The Reactivity Trinity
Vue's reactivity system consists of three core concepts:
- Reactive data - The source of truth (Proxies from Part 1)
- Effects - Functions that depend on reactive data
- Scheduler - Coordinates when effects run
// The core reactivity loop
reactive data → track dependencies → trigger effects → schedule updates → re-run effects
The Dependency Tracking System
At the heart of Vue's reactivity is the dependency tracking system:
type Dep = Set<ReactiveEffect> & {
w?: number; // wasTracked
n?: number; // newTracked
};
type KeyToDepMap = Map<any, Dep>;
const targetMap = new WeakMap<any, KeyToDepMap>();
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown
) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let deps: (Dep | undefined)[] = [];
// Collect all effects that need to run
if (key !== void 0) {
deps.push(depsMap.get(key));
}
// Trigger all collected effects
const effects: ReactiveEffect[] = [];
for (const dep of deps) {
if (dep) {
effects.push(...dep);
}
}
triggerEffects(new Set(effects));
}
The ReactiveEffect Class
Effects are the foundation of Vue's reactivity:
export class ReactiveEffect<T = any> {
active = true;
deps: Dep[] = [];
parent: ReactiveEffect | undefined = undefined;
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope);
}
run() {
if (!this.active) {
return this.fn();
}
let parent: ReactiveEffect | undefined = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
// Clean up old dependencies
cleanupEffect(this);
// Run the effect and collect new dependencies
return this.fn();
} finally {
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
}
}
stop() {
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
Ref Implementation
Refs wrap primitive values to make them reactive:
export interface Ref<T = any> {
value: T;
[RefSymbol]: true;
}
class RefImpl<T> {
private _value: T;
private _rawValue: T;
public dep?: Dep = undefined;
public readonly __v_isRef = true;
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
export function ref<T>(value: T): Ref<UnwrapRef<T>> {
return createRef(value, false);
}
Computed Implementation
Computed properties are cached effects:
export class ComputedRefImpl<T> {
public dep?: Dep = undefined;
private _value!: T;
public readonly effect: ReactiveEffect<T>;
public readonly __v_isRef = true;
public _dirty = true;
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
}
get value() {
// Track the computed as a dependency
trackRefValue(this);
// Only recompute if dirty
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run()!;
}
return this._value;
}
set value(newValue: T) {
this._setter(newValue);
}
}
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>;
let setter: ComputedSetter<T>;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = NOOP;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter, isReadonly) as any;
}
Watch Implementation
Watchers are effects with additional options:
export function watch<T>(
source: WatchSource<T> | WatchSource<T>[],
cb: WatchCallback<T>,
options?: WatchOptions
): WatchStopHandle {
return doWatch(source, cb, options);
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = {}
): WatchStopHandle {
let getter: () => any;
// Normalize the source into a getter function
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => source;
deep = true;
} else if (isArray(source)) {
getter = () =>
source.map((s) => {
if (isRef(s)) return s.value;
if (isReactive(s)) return traverse(s);
if (isFunction(s)) return s();
});
} else if (isFunction(source)) {
getter = () => source();
}
let cleanup: () => void;
const onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
fn();
};
};
let oldValue: any;
const job: SchedulerJob = () => {
if (!effect.active) return;
if (cb) {
const newValue = effect.run();
if (deep || hasChanged(newValue, oldValue)) {
if (cleanup) cleanup();
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
}
} else {
effect.run();
}
};
// Create the effect
const effect = new ReactiveEffect(getter, scheduler);
// Initial run
if (cb) {
if (immediate) {
job();
} else {
oldValue = effect.run();
}
} else {
effect.run();
}
return () => {
effect.stop();
};
}
The Scheduler
The scheduler coordinates when effects run:
const queue: SchedulerJob[] = [];
let isFlushing = false;
let isFlushPending = false;
export function queueJob(job: SchedulerJob) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
nextTick(flushJobs);
}
}
function flushJobs() {
isFlushPending = false;
isFlushing = true;
// Sort jobs by id to ensure parent components update before children
queue.sort((a, b) => getId(a) - getId(b));
try {
for (let i = 0; i < queue.length; i++) {
const job = queue[i];
if (job && job.active !== false) {
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER);
}
}
} finally {
queue.length = 0;
isFlushing = false;
}
}
Putting It All Together
Here's how it all works in a real component:
<script setup lang="ts">
import { ref, computed, watch, watchEffect } from 'vue';
// 1. Create reactive state (Proxy-based)
const count = ref(0);
const doubled = computed(() => count.value * 2);
// 2. Set up watchers (ReactiveEffects with schedulers)
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
watchEffect(() => {
console.log(`Doubled: ${doubled.value}`);
});
// 3. Trigger updates
function increment() {
count.value++; // Triggers deps → schedules effects → batches updates
}
</script>
Key Takeaways
- Vue's reactivity system is built on effects, dependency tracking, and scheduling
refwraps primitives, tracking access via get/setcomputedcreates cached effects that only recompute when dependencies changewatchcreates effects with fine-grained control and callbacks- The scheduler batches and optimizes effect execution
- All reactive APIs (
ref,computed,watch) are built on the same foundation - Understanding this system helps debug reactivity issues and optimize performance
Series Conclusion
Throughout this series, we've explored:
- The Proxy Foundation - How Proxies enable reactivity
- The Language Engine - TypeScript integration and type safety
- The Safety Valve - Error handling and fault tolerance
- The Identity Crisis - Key system and virtual DOM diffing
- Reactive Realities - The complete reactivity system
You now have a deep understanding of Vue 3's internals! This knowledge will help you:
- Write more efficient Vue code
- Debug complex reactivity issues
- Make informed architectural decisions
- Contribute to Vue's ecosystem
Thank you for joining me on this journey into Vue's internals!
This is the final part of the "Vue but underthehood" series. I hope you enjoyed the deep dive!
