Reactive Revolution: Unveiling Vue 3’s Proxies vs. Vue 2’s DefineProperty

Alex U
6 min readApr 23, 2024

--

While it may seem that I’m a bit late to the party, the innovations within Vue.js 3 remain as relevant and exciting as ever. Though Vue 3 is no longer the new kid on the block, its adoption of a proxy-based reactivity system continues to revolutionize frontend development. Join me on a journey as we dive into the inner workings of Vue 3’s reactivity system.

Photo by Leon Seibert on Unsplash

With the release of Vue 3, developers were introduced to a paradigm shift in its reactivity system, moving away from the traditional Object.defineProperty approach to a more robust and versatile proxy-based system.

Reactivity lies at the heart of Vue.js, enabling developers to create interactive user interfaces by automatically updating the DOM when data changes. Vue 2 leveraged Object.defineProperty to achieve this, allowing the framework to track changes to data properties and trigger re-renders accordingly. While effective, this approach had limitations, especially when dealing with deeply nested objects or dynamically added properties.

Vue 3’s adoption of the ECMAScript 6 Proxy API represents a significant evolution in how reactivity is handled within the framework. Proxies provide a more intuitive and flexible way to observe and react to changes in data, offering improved performance and a simpler syntax for developers.

Now that we’ve set the stage with Vue 3’s, let’s take a step back and examine how Vue 2 approached reactivity.

Vue 2 approach

Vue 2’s reactivity system relied on the Object.defineProperty method to achieve its magic. This method allowed Vue to define getter and setter functions for each property in the data object, enabling it to track changes and trigger re-renders as necessary.

Let’s take a closer look at how this worked with a simple example:

// Vue 2 Example
const vm = new Vue({
data() {
return {
message: 'Hello, Vue!'
};
}
});

// Update the message
vm.message = 'Hello, Vue 2!';

In this example, when we create a new Vue instance (vm) with a data property message, Vue internally converts this property into a getter/setter using Object.defineProperty. This allows Vue to intercept any attempts to read or write to message, ensuring that it can track changes.

When we update the message property with vm.message = 'Hello, Vue 2!', Vue's setter function is triggered. Vue then detects the change and re-renders the relevant parts of the DOM to reflect the updated data.

Under the hood Vue converts it into a getter/setter using Object.defineProperty. Here's a simplified version of how this conversion might look:

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// Track dependency here
console.log(`Getting ${key}: ${val}`);
return val;
},
set(newVal) {
// Trigger updates here
console.log(`Setting ${key} to ${newVal}`);
val = newVal;
// Trigger reactivity updates
}
});
}

const data = {};

defineReactive(data, 'message', 'Hello, Vue!');

// Access the property
console.log(data.message); // Output: Getting message: Hello, Vue!

// Update the property
data.message = 'Hello, Vue 2!'; // Output: Setting message to Hello, Vue 2!

In this example, the defineReactive function is responsible for converting a property into a getter/setter pair. When accessing or updating the message property, the corresponding getter or setter is invoked, allowing Vue to track dependencies and trigger updates accordingly.

While this approach worked well for many use cases, it had its limitations. Deeply nested objects or dynamically added properties could pose challenges, as Vue had to recursively convert each property into a getter/setter, leading to performance overhead.

Vue 3 approach

Now, let’s explore how Vue 3’s proxy-based reactivity system provides a more elegant and efficient solution to these challenges.

Unlike Vue 2, which relied on Object.defineProperty, Vue 3 leverages the Proxy API provided by modern JavaScript to achieve reactivity. Proxies offer a more intuitive and flexible way to observe and react to changes in data, leading to improved performance and simpler syntax.

For example in Vue 3, when you create a reactive object using reactive, Vue wraps the object in a Proxy. This Proxy intercepts operations such as property access, assignment, deletion, and more, allowing Vue to track dependencies and trigger updates as needed.

Let’s take a look at how this works with a simple example:

import { reactive } from 'vue';

// Vue 3 Example
const state = reactive({
message: 'Hello, Vue 3!'
});

// Access the property
console.log(state.message); // Output: Hello, Vue 3!

// Update the property
state.message = 'Hello, Vue 3 with Proxies!';

// The reactivity system automatically triggers updates

And here’s a simplified version of how Vue 3’s reactivity system works under the hood:

function createReactiveProxy(target, handler) {
return new Proxy(target, handler);
}

const reactiveHandler = {
get(target, key, receiver) {
// Track dependency
console.log(`Getting ${key}: ${target[key]}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// Trigger updates
console.log(`Setting ${key} to ${value}`);
const result = Reflect.set(target, key, value, receiver);
// Trigger reactivity updates
return result;
}
};

function reactive(obj) {
return createReactiveProxy(obj, reactiveHandler);
}

// Vue 3 Example
const state = reactive({
message: 'Hello, Vue 3!'
});

// Access the property
console.log(state.message); // Output: Getting message: Hello, Vue 3!

// Update the property
state.message = 'Hello, Vue 3 with Proxies!'; // Output: Setting message to Hello, Vue 3 with Proxies!

In this example, the createReactiveProxy function creates a Proxy around the target object (obj) with a given handler. The reactiveHandler object defines how the Proxy should behave for various operations like property access and assignment.

When accessing or updating a property on the reactive object (state), Vue's Proxy intercepts these operations using the get and set traps defined in the reactiveHandler. Inside these traps, Vue tracks dependencies and triggers updates accordingly, ensuring reactive behavior.

Comparison:

To better understand the differences between the two approaches, let’s compare the Object.defineProperty approach used in Vue 2 with Vue 3's proxy-based approach across key dimensions such as performance, syntax, support for dynamic properties, handling of complex data structures, and ecosystem compatibility

Performance:

  • Object.defineProperty: In Vue 2, defining getters and setters using Object.defineProperty involves creating a new accessor property for each data property. This can lead to performance overhead, especially when dealing with large data objects or frequent property updates.
  • Proxy-Based: Vue 3’s proxy-based approach leverages JavaScript Proxies, which are more efficient at intercepting property access and modification. Proxies provide a lightweight mechanism for reactivity, resulting in better overall performance compared to Vue 2’s approach.

Support for Dynamic Properties

  • Object.defineProperty: Vue 2’s reactivity system may not automatically track changes to dynamically added or deleted properties. Developers often need to use workarounds or additional plugins to ensure reactivity for dynamically modified properties.
  • Proxy-Based: Vue 3’s proxy-based approach handles dynamically added or deleted properties seamlessly. Proxies automatically track changes to all properties, including dynamically added ones, ensuring consistent reactivity without the need for extra configuration.

Complex Data Structures

  • Object.defineProperty: Vue 2’s reactivity system may encounter limitations when dealing with deeply nested objects or arrays. Defining getters and setters for nested properties manually can be cumbersome and error-prone.
  • Proxy-Based: Vue 3’s proxy-based approach excels at handling complex data structures such as nested objects and arrays. Proxies automatically track changes to nested properties and arrays, providing more robust reactivity without the need for manual intervention.

Ecosystem Compatibility

  • Both approaches are compatible with the broader JavaScript ecosystem, but Vue 3’s proxy-based approach aligns more closely with modern JavaScript standards. Proxies are a native feature of JavaScript, making Vue 3’s reactivity system more familiar to developers and easier to integrate with other libraries and tools.

In conclusion, Vue 3’s adoption of a proxy-based reactivity system marks a significant milestone in the evolution of frontend development. By embracing modern JavaScript standards and leveraging the power of Proxies, Vue 3 offers developers a more efficient, intuitive, and versatile framework for building dynamic web applications.

The transition from Vue 2’s Object.defineProperty approach to Vue 3's proxy-based approach represents more than just a technical upgrade—it signifies a fundamental shift towards a more elegant and effective way of managing reactivity in Vue applications. With improved performance, simpler syntax, better support for dynamic properties, and enhanced compatibility with complex data structures, Vue 3 empowers developers to build faster, more maintainable, and more scalable applications.

As we continue to explore and embrace the capabilities of Vue 3, it’s clear that its proxy-based reactivity system is not just a feature, but a cornerstone of Vue’s growth and innovation.

Thanks again for reading, and until next time!

--

--