preface

On April 17, UVU announced the official release of Vue 3.0 beta on weibo.

In the Vue3 Design Process article published by UVU, it was mentioned that one of the considerations for reconstructing Vue was how well JavaScript’s new language features were supported in mainstream browsers, most notably Proxy, which provides the framework with the ability to intercept operations on objects. One of the core capabilities of Vue is to listen for user-defined state changes and refresh the DOM in response. Vue 2 implements this feature by replacing getters and setters for state object properties. By changing to a Proxy, you can overcome Vue’s current limitations, such as the inability to listen for new attributes, 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 an advanced front-end ape, we need to know why and why, so let’s take a look at what is Proxy?

What is a Proxy?

The word Proxy translates to “agent” and is used here to mean that it “proxies” certain operations. Proxy creates a layer of “interception” in front of the target object. All external access to the object must pass this layer. Therefore, Proxy provides a mechanism for filtering and rewriting external access.

Let’s take a look at the basic syntax of proxy

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

Let’s look at 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 non-existent attributes also work

person.name // The original object of muyao remains unchanged
Copy the code

In the code above, the configuration object has a GET method that intercepts access requests to properties of the target object. The two arguments to 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 yields 35

Note that Proxy does not change the original object but generates a new one, and for Proxy to work, you must operate on the Proxy instance (proxyPerson in the example above), not the target object (Person in the example above)

The Proxy supports 13 interception operations:

  • get(target, propKey, receiver): intercepts the reading of object properties, such asproxy.fooproxy['foo'].
  • set(target, propKey, value, receiver): Intercepts the setting of object properties, such asproxy.foo = vproxy['foo'] = v, returns a Boolean value.
  • has(target, propKey)Intercept:propKey in proxyReturns a Boolean value.
  • deleteProperty(target, propKey)Intercept:delete proxy[propKey]Returns a Boolean value.
  • ownKeys(target)Intercept:Object.getOwnPropertyNames(proxy),Object.getOwnPropertySymbols(proxy),Object.keys(proxy),for... inLoop to return an array. This method returns the property names of all of the target object’s own properties.
  • getOwnPropertyDescriptor(target, propKey)Intercept:Object.getOwnPropertyDescriptor(proxy, propKey)Returns the description object of the property.
  • defineProperty(target, propKey, propDesc)Intercept:Object. DefineProperty (proxy, propKey propDesc),Object.defineProperties(proxy, propDescs), returns a Boolean value.
  • preventExtensions(target)Intercept:Object.preventExtensions(proxy), returns a Boolean value.
  • getPrototypeOf(target)Intercept:Object.getPrototypeOf(proxy), returns an object.
  • isExtensible(target)Intercept:Object.isExtensible(proxy), returns a Boolean value.
  • setPrototypeOf(target, proto)Intercept:Object.setPrototypeOf(proxy, proto), returns a Boolean value.
  • apply(target, object, args): Intercepts operations called by Proxy instances as functions, such asproxy(... args),proxy.call(object, ... args),proxy.apply(...).
  • construct(target, args): intercepts operations called by Proxy instances as constructors, such asnew proxy(... args).

Why Proxy?

Vue2 change detection

In Vue2, all properties in data are recursively iterated, and all properties are converted to getters/setters using Object.defineProperty. Listen for changes to the data in the setter and notify the place to subscribe to the current data.

// Deeply traverse the data in data, adding 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) {
         // Do dependency collection
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // If the array is an array, each member needs to be collected by dependency. If the array is an array, the member recurses.
            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 must be observed again to ensure that the data is responsivechildOb = ! shallow && observe(newVal)// Notify all observers of data changes
      dep.notify()
    }
  })
Copy the code

However, this implementation has several problems due to the limitations of JavaScript:

  • Unable to detect the addition or removal of object attributes, we need to use vue. set and vue. delete to ensure that the response system works as expected
  • Cannot monitor array subscript and array length changes, when the array index is directly set value or change the array length, cannot respond in real time
  • Performance issues. When there is a lot of data in the data and the hierarchy is very deep, the performance will degrade because all the data in the data must be traversed and set to be responsive

Vue3 improvement

Vue3 has been improved to use Proxy as the new change detection instead of 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
});
Copy the code

We are source vue – next/packages/reactivity/SRC/reactive. Ts file saw the realization of the following:

//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; }Copy the code

Now let’s look at what happens when state is processed

Get (), set(), deleteProperty(), has(), ownKeys())

get()

Get () automatically reads the response data and makes the track call

function createGetter(isReadonly = false, shallow = false) {
  return functionget(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
}
  
Copy the code

set()

When a property is set to a value that does not exist on the target object, the “add” operation is performed and trigger() is triggered to notify the responding system of the update. Fixed an issue where object attributes could not be added 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 the default behavior
    const result = Reflect.set(target, key, value, receiver)
    // Do not trigger if the target object is on the prototype chain
    if (target === toRaw(receiver)) {
      // Add if the set attribute is not on the target object. This solves the problem that Vue 2.x cannot detect the addition or deletion of object attributes
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

deleteProperty()

The associated DELETE operation triggers () to notify the responding system of the update when the property on the target object is deleted. This also solves the problem that deletion of object attributes cannot be detected 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 exists when attributes are deleted
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code

From the () and ownKeys ()

These handlers do not change the default behavior, but they both call track(). Recall that has() affects in, and ownKeys() affects for… And in circulation

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)
}
Copy the code

Through the above analysis, we can see that Vue3 uses several handlers of Proxy to intercept operations and collect dependencies, thus realizing the core of the response system.

What else can a Proxy do?

We have already 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 complicated things before.

Implementing access logging

let api = {
  getUser: function(userId) {
    / *... * /
  },
  setUser: function(userId, config) {
    / *... * /}};/ / call 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); / / call log
      return Reflect.apply(value, target, arguments); }; }}); api.getUsers();Copy the code

Check module

let numObj = { count: 0.amount: 1234.total: 14 };
numObj = new Proxy(numObj, {
  set(target, key, value, proxy) {
    if (typeofvalue ! = ='number') {
      throw Error('Properties in numObj can only be numbers');
    }
    return Reflect.set(target, key, value, proxy); }});// Throws an error because "foo" is not a number
numObj.count = 'foo';
// Assignment succeeded
numObj.count = 333;
Copy the code

You can see that Proxy can have a lot of interesting applications, we quickly explore!


This article was first published on the public number – front-end talk