Mastering Vue's Reactivity: From Object.defineProperty to Proxy

This article explains the fundamentals of Vue's reactivity system, comparing Vue2's Object.defineProperty approach with Vue3's Proxy-based implementation, and walks through building a custom dependency-collection model with code examples, key-level tracking, and branch-aware cleanup to achieve precise reactive updates.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Mastering Vue's Reactivity: From Object.defineProperty to Proxy

Responsive Basics

Reactivity means that when data changes, the system automatically updates the related DOM structure.

In Vue2 the reactive system is built on Object.defineProperty, which cannot monitor array changes, requires traversing each property, and incurs significant performance overhead.

Vue3 switches to the ES6 Proxy object, which can directly observe objects and arrays without per‑property traversal, greatly improving performance and solving the array‑monitoring limitation of Object.defineProperty.

The core of reactivity lies in Vue's dependency‑collection mechanism.

Simplified Model

To illustrate Vue's dependency collection, consider a simple watcher that calls external functions:

function watcher () {
    console.log('watcher start');
    函数1();
    函数2();
    console.log('watcher end');
}

Can we design a dependency‑collection system so that when those external functions run, the watcher also runs?

Key question: How to determine the call relationship between functions?

Example functions:

function A() { console.log('A'); }
function B() { console.log('B'); }
function C() { console.log('C'); }
function watcher () {
    console.log('watcher start!');
    // calls some of the above functions
    console.log('watcher end!');
}
watcher();

From the output we see that A and B are called inside the watcher, while C may be called outside.

Observation: Any function executed during the watcher’s run is a dependency of the watcher.

Based on this principle, a simple dependency‑collection system can be built:

let activeEffect = null;
function effect (watcher) {
    activeEffect = watcher;
    watcher(true);
    activeEffect = null;
}
function A (isTracking = false) {
    if (isTracking) {
        A.effects = A.effects || new Set();
        A.effects.add(activeEffect);
    } else {
        console.log('A触发了');
        A.effects && A.effects.forEach(fn => fn(true));
    }
}
function B (isTracking = false) {
    // similar to A
}

Testing shows the basic mechanism works.

Optimized Dependency System

let activeEffect = null;
function effect (watcher) {
    activeEffect = watcher;
    watcher(true);
    activeEffect = null;
}
const bucket = new WeakMap();
function track (target) {
    const effects = bucket.get(target) || new Set();
    activeEffect && effects.add(activeEffect);
    bucket.set(target, effects);
}
function trigger (target) {
    bucket.get(target)?.forEach?.(fn => fn(true));
}
function A (isTracking = false) {
    if (isTracking) {
        track(A);
    } else {
        console.log('A触发了');
        trigger(A);
    }
}
function B (isTracking = false) {
    // implementation omitted
}

Now the collection and triggering logic are extracted into track and trigger functions.

Vue Dependency Collection Model

Vue3 uses Proxy for dependency collection. The get trap collects dependencies, and the set trap triggers them.

const data = { value: 1 };
const proxyData = new Proxy(data, {
    get(target, key) {
        track(target);
        return target[key];
    },
    set(target, key, value) {
        trigger(target);
        target[key] = value;
    }
});

Testing shows the basic reactivity works, but a non‑related key assignment also triggers watchers, which is undesirable.

2. Key‑Level Dependencies

To achieve fine‑grained tracking, we record dependencies per object property:

function track (target, key) {
    const effects = bucket.get(target) || new Map();
    const keyMap = effects.get(key) || new Set();
    effects.set(key, keyMap);
    bucket.set(target, effects);
    activeEffect && keyMap.add(activeEffect);
}
function trigger (target, key) {
    const effects = bucket.get(target);
    if (!effects) return;
    const keyMap = effects.get(key);
    if (!keyMap) return;
    keyMap.forEach(effect => effect());
}
const data = { value: 1 };
const proxyData = new Proxy(data, {
    get(target, key) {
        track(target, key);
        return target[key];
    },
    set(target, key, value) {
        trigger(target, key);
        target[key] = value;
    }
});

This implements precise property‑level listening.

However, when a branch condition changes (e.g., a boolean flag toggles), previously collected dependencies may become stale, causing unnecessary reactions.

3. Branch Switching

Vue solves this by clearing a watcher’s dependencies before each run and rebuilding them during execution, ensuring only current dependencies are tracked.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Proxyfrontend developmentVueReactivityDependency Collection
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.