22 Structural Vue.js How to Implement Reactive Programming Through Proxy

22 Structural Vue #

Hello, I’m Ishikawa.

In the previous lesson, we introduced several different creational patterns. Today, let’s talk about the structural patterns in design patterns. Among the structural patterns, the most classic and commonly used one is the proxy pattern. In JavaScript development, the proxy pattern is also a frequently used design pattern.

Front-end developers should be familiar with Vue.js. One of the great features of Vue.js is its use of reactive programming, which is currently popular. So how does it achieve this? It is closely related to the proxy pattern. In this lesson, we will mainly answer this question. But before we unveil the mystery, let’s take a look at some traditional and intuitive application scenarios and implementation methods of the proxy pattern.

Applications of Proxy #

In the Proxy Design Pattern, a proxy object acts as an interface for another subject object. Many people compare the Proxy Pattern with the Facade Pattern. In the following lectures, we will introduce the Facade Pattern. Here, we need to understand that the Proxy Pattern is different from the Facade Pattern. The main function of the Facade Pattern is to simplify the interface design, hide the implementation of complex logic behind the scenes, and combine different method calls into more convenient methods. On the other hand, the proxy object plays a role between the caller and the subject object, mainly to protect and control the caller’s access to the subject object. The proxy intercepts all or some of the operations to be performed on the subject object, and sometimes enhances or supplements its behavior.

Image

As shown in the above figure, the proxy and the subject usually have the same interface, which is transparent to the caller. The proxy forwards each operation to the subject and enhances its behavior through additional preprocessing or post-processing. This pattern may seem like a “middleman”, but it is reasonable and the proxy, especially in terms of performance optimization, plays a significant role. Let’s take a closer look at each application.

Lazy Initialization and Caching #

Because the proxy acts as the protection of the subject object, it reduces the client’s consumption of the subject object that is actually ineffective behind the proxy. It’s like the salespeople in a company, who do not directly pass all customer needs to the project team for development. Instead, when they receive a customer’s request, they first provide the customer with a product service manual. When the customer really chooses a service and confirms the purchase, then the pre-sales will transfer to the project team to implement and deliver the results to the customer.

Image

This example is an application called lazy initialization. In this case, the proxy can act as an interface to provide assistance. The proxy accepts initialization requests and does not pass them to the subject object until it is clear that the caller will actually use the subject. The client sends an initialization request and the proxy responds, but in fact, it does not pass the message to the subject object until the client obviously needs the subject to complete some work. Only in this case, the proxy will pass both messages together.

On this basis, we can see the second example. In this example, in addition to playing the role of lazy initialization, the proxy can also add a caching layer. When the client makes the first access, the proxy will consolidate the request to the subject and cache the result before returning it separately to the client. When the client initiates request 2 for the second time, the proxy can directly read the information from the cache without accessing the subject, and then return it to the client directly.

Image

Other Applications of Proxy #

As a web language, JavaScript often deals with network requests, and it can play a significant role in performance optimization based on the above methods. In addition to lazy initialization and caching, the proxy also has important uses in many other aspects. For example, data validation, the proxy can validate the content of the input before forwarding it to the subject, ensuring its correctness before passing it to the backend. In addition, the proxy pattern can also be used for security verification. The proxy can be used to verify whether the client is authorized to perform certain operations, and only send the request to the backend when the check result is positive.

Another application of the proxy is logging. The proxy can intercept method calls and related parameters, and re-encode them. Moreover, it can also obtain remote objects and store them locally. After discussing the applications of the proxy, we can now take a look at its implementation in JavaScript.

Implementation of Proxy #

There are many ways to implement the Proxy pattern in JavaScript. These include: 1. Object composition or object literal with factory pattern; 2. Object augmentation; 3. Using the built-in Proxy feature introduced in ES6. Each of these approaches has its own advantages and disadvantages.

Composition Pattern #

Let’s first look at the composition pattern. As we discussed in the previous section, based on the principles of functional programming, we should strive to maintain the immutability of the main object in programming. Based on this principle, composition can be considered as a simple and safe way to create a proxy, as it keeps the main object unchanged and does not alter its original behavior. The only drawback is that we have to manually delegate all the methods, even if we only want to proxy one of them. Additionally, we may need to delegate access to the properties of the main object. Also, if we want to achieve lazy initialization, composition is basically the only option. Here’s a pseudo code demonstration:

class Calculator {
  constructor() {
    /*...*/
  }
  plus() { /*...*/ }
  minus() { /*...*/ }
}

class ProxyCalculator {
  constructor(calculator) {
    this.calculator = calculator;
  }
  // Proxy methods
  plus() { return this.calculator.divide(); }
  minus() { return this.calculator.multiply(); }
}

var calculator = new Calculator();
var proxyCalculator = new ProxyCalculator(calculator);

Apart from the above approach, we can also use the factory function based on the composition approach to create a proxy.

function factoryProxyCalculator(calculator) {
  return {
    // Proxy methods
    plus() { return calculator.divide(); },
    minus() { return calculator.multiply(); }
  };
}

var calculator = new Calculator();
var proxyCalculator = new factoryProxyCalculator(calculator);

Object Augmentation #

Now let’s talk about the second approach, object augmentation (also known as Monkey Patching). The advantage of object augmentation is that it doesn’t require delegating all the methods. However, the biggest problem is that it changes the main object. Using this approach does simplify the creation of proxies, but the drawback is that it introduces “side effects” in the functional programming sense, as the main object no longer maintains immutability.

function patchingCalculator(calculator) {
  var plusOrig = calculator.plus;
  calculator.plus = () => {
    // Additional logic
    // Delegate to the main object
    return plusOrig.apply(calculator);
  };
  return calculator;
}
var calculator = new Calculator();
var safeCalculator = patchingCalculator(calculator);

Built-in Proxy #

Finally, let’s take a look at using the built-in Proxy feature in ES6. Starting from ES6, JavaScript has native support for Proxy. It combines the advantages of both object composition and object augmentation, as we don’t need to manually delegate all the methods and the main object remains unchanged, maintaining the immutability of the main object. However, it also has a drawback, which is that it has almost no polyfill. In other words, if you use the built-in proxy, you have to make some sacrifices in terms of compatibility. You really can’t have your cake and eat it too.

var ProxyCalculatorHandler = {
  get: (target, property) => {
    if (property === 'plus') {
      // Proxy method
      return function () {
        // Additional logic
        // Delegate to the main object
        return target.divide();
      };
    }
    // Delegated methods and properties
    return target[property];
  }
};
var calculator = new Calculator();
var proxyCalculator = new Proxy(calculator, ProxyCalculatorHandler);

How to achieve reactive programming with proxies in Vue.js #

In our previous lesson on the singleton pattern, we discussed how some third-party libraries, such as Redux and reducers, incorporate functional programming to solve problems in traditional object-oriented programming. Today, let’s analyze another library, namely the state management philosophy of Vue.js. So, how does Vue.js achieve reactive programming with proxies? Here, Vue.js creates a design pattern called Change Observer through proxies.

One of the most distinctive features of Vue.js is its unobtrusive reactivity system. Component state is represented as a reactive JavaScript object, and when it is modified, the UI updates accordingly. It’s similar to using Excel, where if we set a formula in cell A2 to add the values of cells A0 and A1, e.g., “=A0+A1”, changing the values of A0 or A1 will automatically update A2. This represents the concept of side effects frequently mentioned in functional programming.

Image

In JavaScript, if we use imperative programming, we can see that such side effects do not exist.

var A0 = 1;
var A1 = 2;
var A2 = A0 + A1;
console.log(A2) // returns 3
A0 = 2;
console.log(A2) // still returns 3

However, reactive programming is a paradigm based on declarative programming. To achieve reactive programming, we need an update function that updates the value of A2 every time A0 or A1 changes. By doing so, we introduce a side effect, and the update function depends on the values of A0 and A1. These dependencies represent the subscribers of this side effect. The function “whenDepsChange” in the following example is a pseudo-code representation of a subscription mechanism.

var A2;
function update() {
  A2 = A0 + A1;
}
whenDepsChange(update);

In JavaScript, there is no built-in mechanism like “whenDepsChange” to track the read and write operations on local variables. What Vue.js can do is intercept the read and write operations of object properties. JavaScript provides two methods for intercepting property access: getter/setter and proxies. Due to browser support limitations, Vue 2 only uses getter/setter. In Vue 3, proxies are used for reactive objects, while getter/setter is used for accessing the elements of refs. The following pseudo-code illustrates how reactive objects work:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key);
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      trigger(target, key);
    },
  });
}

You might wonder how the interception and customization of getter/setter achieve observation of changes. This is where the handler includes a series of optional methods with predefined names, known as trap methods, such as apply, get, set, and has. These methods are automatically called when corresponding operations are performed on the proxy instance. Therefore, after interception and customization, they will be automatically called when related changes occur in the object. So, assuming we can subscribe to the values of A0 and A1, we can automatically calculate the update of A2 when these two values change.

import { reactive, computed } from 'vue';
var A0 = reactive(0);
var A1 = reactive(1);
var A2 = computed(() => A0.value + A1.value);
A0.value = 2;

Extension: Other Scenarios for Using Proxy #

In addition to serving as a proxy, the built-in Proxy in JavaScript has many other uses. Based on its interception and customization features, Proxy is also widely used for object virtualization, operator overloading, and the recently popular meta-programming. Here, instead of using pseudocode, let’s replace it with some simple real code to see the power of trap methods.

Object Virtualization #

Let’s first take a look at object virtualization. In the example below, the oddNumArr array of odd numbers is a virtual object. We can check if a number is in the odd number array, and we can also get an odd number. But in reality, this array does not store any data.

const oddNumArr = new Proxy([], {
  get: (target, index) => index % 2 === 1 ? index : Number(index)+1,
  has: (target, number) => number % 2 === 1
})

console.log(4 in oddNumArr) // false
console.log(7 in oddNumArr) // true
console.log(oddNumArr[15])   // 15
console.log(oddNumArr[16])   // 17

Operator Overloading #

Operator overloading is the redefinition of existing operators to give them another functionality to adapt to different data types. For example, in the example below, we are overloading the “.” symbol, so when executing obj.count, we see that it returns the custom methods intercepted by the get and set traps, as well as the result of the count.

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`Getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`Setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

obj.count = 1; // Returns: Setting count!
console.log(obj.count);
// Returns: Getting count!
// Returns: Setting count!
// Returns: 1

In addition to object virtualization and operator overloading, the power of Proxy lies in the implementation of meta-programming. However, the topic of meta-programming is too large, and we will dedicate a separate class to discuss it later.

Summary #

Through today’s content, we once again see the intersection of object-oriented, functional, and reactive programming. Classic books on design patterns primarily explain proxies from a purely object-oriented perspective. However, through the opening question, we can see that proxies are also a powerful tool for solving state tracking issues in reactive programming. Therefore, from the proxy-based change observer pattern used in Vue.js, we can also see that no design pattern is absolute, but can be combined to form new patterns to solve problems.

Thinking Questions #

We say that responsive design uses many ideas from functional programming, but it is not purely functional programming. Can you point out some differences between them?

Feel free to share your answers, exchange learning experiences, or ask questions in the comments section. If you find this helpful, please also share today’s content with more friends. See you next time!