If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

Vue Proxy and Reflect

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

Built-in object Reflect

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:

  1. Read property value: obj.foo (reads the value of property foo)
  2. Set property value: obj.foo = newValue (sets the value of property foo)

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.
```

Vue Shallow Reactivity vs Deep Reactivity vue-expired-side-effects

Comments