How Vue 3’s Proxy‑Based Reactivity Beats Vue 2’s Object.defineProperty
This article explains Vue’s reactive system, compares Vue 2.6’s Object.defineProperty approach with Vue 3’s Proxy implementation, details how observers are defined, collected, and triggered, and shows why the asynchronous update queue improves performance and maintainability.
What is the reactive system
In Vue, a reactive system automatically re‑executes code when the data it depends on changes. When a template accesses a data property and that property is later mutated, Vue re‑renders the view.
The core idea is that a piece of code references a variable, and modifying that variable automatically triggers the code.
Vue distinguishes two roles: reactive data (the mutable state) and observers (the code that runs when the state changes).
Reactive data – the values you can modify.
Observer – the code executed after a data change.
Reactive observation of data
Two key questions arise: how do reactive data and observers connect, and how does a data change automatically trigger observers? The answer is proxy . Vue 3 intercepts get to collect dependencies and set to trigger them.
// proxy handler.get (simplified)
function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // collect observer as dependency
return res;
}
// proxy handler.set (simplified)
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key, value); // trigger collected observers
return result;
}Thus, get collects dependencies via track, and set triggers them via trigger.
Why Vue 3 uses Proxy
Vue 2.x relied on Object.defineProperty for each property, requiring a traversal of all keys. Proxy needs only a single wrapper around the whole object, giving a time‑complexity advantage of n:1 for shallow observation.
var obj = { a: 999, b: 888, c: 777 };
// Using Object.defineProperty you must define a getter/setter for a, b, c individually.
// With Proxy you create one proxy for the whole object.Additional benefits of Proxy include:
Support for adding/removing object keys.
Reactivity for Set and Map structures.
Array index and length tracking.
Definition of observers
Observers are the code that runs after a reactive change. Vue wraps this code in an effect to manage its interaction with reactive data.
Observer collection and triggering
Vue 3 creates a depsMap (a Map) for each observed target. The map’s keys are the target’s properties, and each key maps to a Set of dependent effects.
// collect dependency (simplified)
function track(target, type, key) {
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()));
// ... add activeEffect to dep
}When an observer runs, it becomes activeEffect and is pushed onto effectStack. Only the top of the stack is collected during a get operation.
// effect (observer) function (simplified)
const effect = function reactiveEffect() {
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn(); // run the wrapped function (e.g., render)
} finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
};The stack ensures proper ordering when one observer triggers another.
Observer categories
Computed property observers
Watch option observers
Render function observers
Computed observers act both as reactive data and as observers, and they are lazily evaluated. When a reactive property changes, its dependent computed observer is marked dirty, and the next render triggers its re‑evaluation.
Vue 2 vs Vue 3 observer definitions
Vue 2 uses a Watcher class; Vue 3 extracts shared logic into an effect function, improving maintainability.
// Vue 2 watcher run method (simplified)
function run() {
if (this.active) {
const value = this.get();
if (value !== this.value || isObject(value) || this.deep) {
const oldValue = this.value;
this.value = value;
if (this.user) {
const info = `callback for watcher "${this.expression}"`;
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info);
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
} // Vue 3 effect import (simplified)
import { effect } from './effect';
class ComputedRefImpl {
constructor(getter) {
this.effect = effect(getter, {/* options */});
// ...
}
}Asynchronous update queue
When a reactive change occurs, observers are not executed immediately. Vue batches them in an asynchronous queue so that each observer runs at most once per event loop, preserving a deterministic order.
In Vue 2, a queue stores watchers, deduplicates them, and flushes them via nextTick. The queue is sorted by the watcher’s creation id, giving earlier watchers higher priority.
// Vue 2 queue flushing (simplified)
function flushSchedulerQueue() {
queue.sort((a, b) => a.id - b.id);
queue.forEach(watcher => watcher.run());
}The execution order becomes: computed observer → watch observer → render observer.
Vue 3 uses a similar mechanism: effects are placed into an async queue and cleared by a micro‑task ( flushJobs). Vue 3 further splits the queue into three categories for finer control.
Summary
From the perspective of the reactive system, the upgrade from Vue 2.6 to Vue 3.0 does not overturn the fundamental design; it still consists of data observation, observers, and an asynchronous update queue. The shift to Proxy, the extraction of observer logic into effect, and the refined async queue bring performance gains, better architecture, and easier extensibility for frontend developers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
