Vue 3.0 - Proxy

Posted by tolutlou on Wed, 24 Jun 2020 05:18:02 +0200

This article starts with the official account: qianduansuohua. Welcome.

preface

On April 17, Yuda announced the official release of Vue 3.0 beta on Weibo.

In the article "Vue3 design process" released by Youda, it is mentioned that one of the considerations for refactoring Vue is the support level of JavaScript's new language features in mainstream browsers, and the most noteworthy one is Proxy, which provides the framework with the ability to intercept the operation of objects. One of Vue's core capabilities is to listen for user-defined state changes and refresh the DOM in response. Vue 2 implements this feature by replacing getter s and setter s of state object properties. After changing to Proxy, you can break through the current limitations of Vue, such as being unable to listen to new properties, and provide better performance.

Two key considerations led us to the new major version (and rewrite) of Vue: First, the general availability of new JavaScript language features in mainstream browsers. Second, design and architectural issues in the current codebase that had been exposed over time.

As a high-level front-end ape, we need to know its nature, but also its reason. Let's take a look at what is Proxy?

What is Proxy?

The word Proxy translates to "Proxy" and is used here to indicate that it is used to "Proxy" certain operations.
Proxy will set up a layer of "interception" before the target object. External access to the object must be intercepted through this layer first, so it provides a mechanism to
To filter and rewrite external access.

Let's look at the basic syntax of proxy

const proxy = new Proxy(target, handler)
  • target: the original object you want to proxy (can be any type of object, including a native array, function, or even another proxy)
  • handler: an object that defines which operations will be intercepted and how to redefine the intercepted operations

Let's take a simple example:

const person = {
    name: 'muyao',
    age: 27
};

const proxyPerson = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.name // 35
proxy.age // 35
proxy.sex // 35 attributes that do not exist also work

person.name // muyao original object unchanged

In the above code, the configuration object has a get method to intercept access requests to the properties of the target object. The two parameters of the get method are the target object and the property to be accessed. As you can see, since the interceptor always returns 35, accessing any property gets 35

Note that Proxy does not change the original object but generates a new object. To make Proxy work, you must operate on the Proxy instance (Proxy person in the above example), not the target object (person in the above example)

There are 13 interception operations supported by Proxy:

  • get(target, propKey, receiver): intercept the reading of object properties, such as proxy.foo And proxy['foo '].
  • set(target, propKey, value, receiver): intercepts the setting of object properties, such as proxy.foo =V or proxy['foo'] = v, returns a Boolean value.
  • has(target, propKey): intercepts the operation of propKey in proxy and returns a Boolean value.
  • deleteProperty(target, propKey): intercepts the operation of delete proxy[propKey], and returns a Boolean value.
  • ownKeys(target): intercept Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for...in loops to return an array. This method returns the property names of all the properties of the target object itself.
  • getOwnPropertyDescriptor(target, propKey): intercept Object.getOwnPropertyDescriptor(proxy, propKey), which returns the description object of the property.
  • defineProperty(target, propKey, propDesc): intercept Object.defineProperty(proxy, propKey, propDesc), Object.defineProperties(proxy, propDescs), which returns a Boolean value.
  • preventExtensions(target): intercept Object.preventExtensions(proxy), which returns a Boolean value.
  • getPrototypeOf(target): intercept Object.getPrototypeOf(proxy), which returns an object.
  • isExtensible(target): intercept Object.isExtensible(proxy), which returns a Boolean value.
  • setPrototypeOf(target, proto): intercept Object.setPrototypeOf(proxy, proto), which returns a Boolean value.
  • apply(target, object, args): intercept the operation of Proxy instance as function call, such as proxy(...args) proxy.call(object, ...args), proxy.apply(...).
  • construct(target, args): block the operation called by the Proxy instance as a constructor, such as new proxy(...args).

Why Proxy?

vue2 change detection

In Vue2, all properties in data are traversed recursively, and the Object.defineProperty Turn all these properties into getter/setter, collect and process data dependency in getter, monitor data changes in setter, and notify the place where the current data is subscribed.

// Deeply traverse the data in the data, and add a response to each attribute of the object
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
         // Conduct dependency collection
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // If it is an array, it needs to collect the dependency of each member. If the member of the array is still an array, it is recursive.
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // The new value needs to be observe d again to ensure data responsive
      childOb = !shallow && observe(newVal)
      // Inform all observers of data changes
      dep.notify()
    }
  })

However, due to the limitation of JavaScript, there are several problems in this implementation:

  • The addition or removal of object properties cannot be detected, so we need to use the Vue.set And Vue.delete To ensure that the response system operates as expected
  • The change of array subscript and array length cannot be monitored. When setting value or changing array length directly through array subscript, it cannot respond in real time
  • Performance problem: when there are many data in data and the level is very deep, because to traverse all data in data and set it as responsive, it will lead to performance degradation

Vue3 improvements

Vue3 has made a new improvement, using Proxy proxy as a new change detection, no longer using Object.defineProperty

In Vue3, you can use reactive() to create a response state

import { reactive } from 'vue'

// reactive state
const state = reactive({
  desc: 'Hello Vue 3!',
  count: 0
});

In the source code Vue next / packages / reactivity / SRC/ reactive.ts The following implementation is shown in the file:

//reactive f => createReactiveObject()
function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  ...
  // Set interceptor
  const handlers = collectionTypes.has(target.constructor)
      ? collectionHandlers
      : baseHandlers;
  observed = new Proxy(target, handlers);
  ...
  return observed; 
}

Let's take a look at the processed state

You can see that the five handler s of get(), set(), deleteProperty(), has(), ownKeys() are set in the target object state of the agent. Let's see what they have done together

get()

get() will automatically read the response data and make a track call

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    ...
    // Restore default behavior
    const res = Reflect.get(target, key, receiver)
    ...
    // Call track
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
}
  

set()

When a property setting value does not exist on the target object, an add operation is performed and trigger() is triggered to notify the response system of the update. Solved the problem that adding object properties could not be detected in Vue 2.x

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    ...
    const hadKey = hasOwn(target, key)
    // Restore default behavior
    const result = Reflect.set(target, key, value, receiver)
    // If the target object is on the prototype chain, do not trigger
    if (target === toRaw(receiver)) {
      // If the set property is not on the target object, Add or delete the object property is not detected in Vue 2.x
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

deleteProperty()

Associated with the delete operation, trigger() will be triggered to notify the response system of the update when the attribute on the target object is deleted. This also solves the problem of undetectable deletion of object properties in Vue 2.x

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  
  // Trigger trigger when attribute deletion exists
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

has() and ownKeys()

These two handler s do not modify the default behavior, but they both call the track() function. In retrospect, we can see that has() affects in operation, ownKeys() affects for...in and loop

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)
  return result
}

function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

Through the above analysis, we can see that Vue3 implements the core of the response system with the help of several Handler interception operations of Proxy to collect dependencies.

What else can Proxy do?

We have seen the application scenario of Proxy in Vue3. In fact, after using Proxy, the behavior of objects is basically controllable, so we can use it to do some complex things that were implemented before.

Implement access log

let api = {
  getUser: function(userId) {
    /* ... */
  },
  setUser: function(userId, config) {
    /* ... */
  }
};
// Log
function log(timestamp, method) {
  console.log(`${timestamp} - Logging ${method} request.`);
}
api = new Proxy(api, {
  get: function(target, key, proxy) {
    var value = target[key];
    return function(...arguments) {
      log(new Date(), key); // Log
      return Reflect.apply(value, target, arguments);
    };
  }
});
api.getUsers();

Calibration module

let numObj = { count: 0, amount: 1234, total: 14 };
numObj = new Proxy(numObj, {
  set(target, key, value, proxy) {
    if (typeof value !== 'number') {
      throw Error('Properties in numObj can only be numbers');
    }
    return Reflect.set(target, key, value, proxy);
  }
});

// Error thrown because 'foo' is not a value
numObj.count = 'foo';
// Assignment succeeded
numObj.count = 333;

You can see that Proxy can have many interesting applications. Let's explore it quickly!

Topics: Javascript Vue Attribute