Introduction
Since Vue.js 3’s reactive data is based on Proxy, it’s essential to understand Proxy and its associated concept, Reflect. What is Proxy? In simple terms, Proxy allows you to create a proxy object. It can proxy other objects, emphasizing that Proxy can only proxy objects and not non-object values like strings, booleans, etc. So, what does proxying mean? Proxying refers to the act of creating a basic semantic representation of an object. It allows us to intercept and redefine the basic operations on an object.
Create object proxies with Proxy
When we talk about “basic semantics” in programming languages, we mean the fundamental operations for reading and modifying data. In JavaScript, these operations typically include reading property values and setting property values. For example, given an object obj
, the following operations are considered basic semantics:
- Read property value:
obj.foo
(reads the value of propertyfoo
) - Set property value:
obj.foo = newValue
(sets the value of propertyfoo
)
In the above code, Proxy
objects allow us to intercept (or redefine) these basic semantic operations. The Proxy
constructor takes two parameters: the object being proxied and an object containing interceptors (also known as traps). In the interceptor object, we can define the get
method to intercept property read operations and the set
method to intercept property set operations. This way, we can execute custom logic when these operations occur.
Understanding these basic semantic operations and how to use Proxy
and Reflect
to intercept and handle them is crucial for implementing reactive data in JavaScript. In reactive data, we can use Proxy
and Reflect
to track reads and modifications of object properties, enabling reactive updates of data.
Basic Usage of Proxy
When we talk about basic semantics, we refer to fundamental operations in JavaScript, such as reading object property values and setting object property values. Consider the following object obj
:
const obj = { foo: 1 };
Here, obj.foo
is a basic semantic operation for reading property values, and obj.foo = newValue
is a basic semantic operation for setting property values.
Now, we can use Proxy
to intercept these basic semantic operations.
const handler = {
get(target, key) {
console.log(`Reading property ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`Setting property ${key} to ${value}`);
target[key] = value;
}
};
const proxyObj = new Proxy(obj, handler);
proxyObj.foo; // Outputs: Reading property foo
proxyObj.foo = 2; // Outputs: Setting property foo to 2
In the above code, we created a handler
object that defines get
and set
methods to intercept property reads and sets. Then, we created a proxy object proxyObj
using the Proxy
constructor, which intercepts read and set operations on the obj
object. When we access proxyObj.foo
, the get
method is triggered, outputting the corresponding message. When we set the value of proxyObj.foo
, the set
method is triggered, again outputting the corresponding message.
This way, Proxy
allows us to execute custom logic when basic semantic operations occur, without directly manipulating the original object. In practical applications, this capability can be used to implement reactive data, data validation, logging, and more.
When intercepting object property reads with Proxy
, special attention is required for accessor properties because accessor properties are defined using getter functions. The this
keyword inside these getter functions changes based on the method of invocation.
To solve this issue, we use Reflect.get(target, key, receiver)
instead of target[key]
when accessing property values. This ensures that the receiver
parameter correctly points to the proxy object, not the original object. Consequently, within the getter function of accessor properties, the this
keyword refers to the proxy object, establishing the correct reactive relationship.
Here is the corrected code using Reflect.get
:
const handler = {
get(target, key, receiver) {
track(target, key); // Reactive data dependency tracking
return Reflect.get(target, key, receiver); // Use Reflect.get to get property value
},
// Other interceptor methods...
};
const proxyObj = new Proxy(obj, handler);
effect(() => {
console.log(proxyObj.bar); // Access the bar property inside the side effect function
});
proxyObj.foo++; // Triggers re-execution of the side effect function
In this code, we use Reflect.get
with the receiver
parameter to ensure that this
points to the proxy object within the get
interceptor function. This establishes the correct reactive relationship, allowing proper dependency tracking when accessing object properties.
Usage of Reflect in Reactivity
In interceptor functions, we aim to establish a connection between side-effect functions and reactive data. This ensures that when properties are accessed, the correct dependencies are tracked, enabling re-execution of side-effect functions when properties change. However, if we directly use target[key]
to access property values, the this
keyword inside the getter function of accessor properties points to the original object, not the proxy object. This prevents the establishment of the correct reactive relationship.
To address this issue, we use Reflect.get(target, key, receiver)
instead of target[key]
. By doing so, the receiver
parameter correctly points to the proxy object, allowing the this
keyword inside the getter function to refer to the proxy object. This establishes the proper reactive relationship.
Here is an example demonstrating the use of the receiver
parameter and comparing it with the scenario where the receiver
parameter is not used:
1. Using the receiver
parameter:
const data = {
foo: 1
};
const proxy = new Proxy(data, {
get(target, key, receiver) {
// Use Reflect.get to ensure `this` points to the proxy object
const result = Reflect.get(target, key, receiver);
// Additional processing, such as triggering update operations, can be performed in practical applications
console.log(`Accessed ${key} property with value ${result}`);
return result;
}
});
console.log(proxy.foo); // Outputs: Accessed foo property with value 1
In this example, we use the receiver
parameter passed to Reflect.get
to ensure that this
inside the get
interceptor function refers to the proxy object proxy
. When you access proxy.foo
, the get
interceptor function is triggered, and this
points to the proxy
object.
2. Not using the receiver
parameter:
const data = {
foo: 1
};
const proxy = new Proxy(data, {
get(target, key) {
// Without using the receiver parameter, 'this' refers to the original object 'data'
const result = target[key];
// In practical applications, additional processing might be required, such as triggering update operations
console.log(`Accessed ${key} property with value ${result}`);
return result;
}
});
console.log(proxy.foo); // Output: Accessed foo property with value 1
In this example, we did not use the receiver
parameter. Since the receiver
parameter was not passed, this
inside the get
interceptor function points to the original object data
. Although the proxy object proxy
is used, the this
inside the get
interceptor function does not refer to proxy
but instead refers to the original object data
. Therefore, in this scenario, the reactive relationship is not established.
While the output of the two functions is the same, it’s evident that without using the receiver
parameter, the reactive relationship is not established. This means that within the effect
function, the object will not receive the correct reactivity.
Note: This article is a translated version of the original post. For the most accurate and up-to-date information, please refer to the original source.
```
Comments