Object. DefineProperty implements responsiveness

The first thing you need to know is that Object.defineProperty only listens for objects, and this Object does not refer to an Object type (arrays are also Object types), but rather an Object constructor Object, which is {}.

The structure is divided into three parts:

  • UpdateView: A function to update a view
  • DefineReactive: A function that listens for object data changes
  • Observer: Decompose each attribute of the data for deep listening

Listening to the object

Vue2. X implements responsive view updates, and the process is as follows:

Function observer(target) {if (typeof Target); function updateView() {console.log(" view update ")} function observer(target) {if (typeof Target) ! = = "object" | | target = = = null) {/ / not object or an array of return target} / / monitor target for each attribute (let the key in the target) { DefineReactive (target, key, target[key])}} function defineReactive(target, key, value) { Observer (value) // Core API: Object.defineProperty() Object.defineProperty(target, key, { get() { return value }, set(newValue) { if (newValue ! Observer (newValue) == value) {// set a newValue observer(newValue); // set a newValue observer(newValue) Value = newValue // updateView()}},})}Copy the code

Due to JavaScript limitations, data in Vue cannot be dynamically added with root-level responsive properties. That is, all root-level responsive properties must be declared before the instance is initialized, even if it is a null value, otherwise the view will not be updated even if the value has changed.

You can run it through the code above. 👆

Listening to the array

To listen for arrays, Vue2 overwrites part of the array method to implement view updates, but this part has nothing to do with Object.defineProperty.

👇 is roughly implemented like this:

Function updateView() {console.log(" view update ")} const oldArrayProperty = array.prototype Const arrPrototype = object.create (oldArrayProperty) const arrPrototype = Object.create(oldArrayProperty) const arrPrototype = Object.create(oldArrayProperty) ["push", "pop", "unshift", "shift", ForEach ((method) => (arrPrototype[method] = function () {arrPrototype[method] = function () { oldArrayProperty[method].call(this, ... Function observer(target) {if (typeof target! = = "object" | | target = = = null) {/ / not object or an Array of return target} / / in order not to pollution global Array prototype: If (array.isarray (target)) {target.__proto__ = arrPrototype} defineReactive(target, key, target[key]) } }Copy the code

Because of object.defineProperty restrictions, manipulation of arrays is very limited in Vue2.

Vue cannot detect changes to the following arrays:

  1. When you set an array item directly using an index, for example:vm.items[indexOfItem] = newValue
  2. When you modify the length of an array, for example:vm.items.length = newLength

The splice method is recommended for adding and deleting arrays because the internal overwrite method overwrites the splice method. Or use Vue. The set () | vm. $set () to add data to the response type.

Proxy and Reflect are responsive

It is important to talk about Proxy before Posting the code. It seems that many people misunderstand Proxy and think that Proxy is just a substitute for Object. DefineProperty in ES5 syntax. Proxy and Object.defineProperty are two completely different things. So Object.defineProperty does not replace Proxy either.

Proxy

Proxy can be interpreted as a layer of interception before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access. The word Proxy is used to mean that it acts as a Proxy for certain operations.

ES6 natively provides a Proxy constructor to generate a Proxy instance.

First go up the code to see the basic operation 👇

Var arr = [1,2] var arrProxy = new Proxy(arr, {get(target, propKey) {console.log(' getting ${propKey}! `) }, set(target, propKey, value) { console.log(`setting ${propKey}! `)}}) / / setting arrProxy [0] = 'change' / / setting 0! ArrProxy [1] // Getting 1!Copy the code

The code above sets up a layer of interception on the ARR array, redefining the read (GET) and set (set) behavior of the property.

As a constructor, Proxy takes two arguments:

  • The first argument is the target object to be proxied (the example above is one)arrObject), that is, if noneProxyThis is what the operation originally wanted to accessarrObject. The object hereisRefers to an object type (array is also an object type).
  • The second parameter is a configuration objecthandlerFor each propped operation, you need to provide a corresponding handler function that intercepts the corresponding operation. For example, in the code above, the configuration object has onegetMethod to intercept access requests to properties of the target object.getThe two arguments to the method are the target object and the property to be accessed.

Note: For Proxy to work, you must operate on the Proxy instance (in the example above, the arrProxy object), not the target object (in the example above, the ARR object).

The following is a list of 13 interception operations supported by Proxy.

  • get(target, propKey, receiver): intercepts the reading of object properties, such asproxy.fooandproxy['foo'].
  • set(target, propKey, value, receiver): Intercepts the setting of object properties, such asproxy.foo = vorproxy['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, andObject.keys()The return result of the object contains only the traversable properties of the target object itself.
  • 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. If the target object is a function, there are two additional operations that can be intercepted.
  • 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).

You can see that the Proxy not only implements object.defineProperties but also intercepts other operations.

My Proxy content is basically a reference to Ruan Yifeng ES6 tutorial, especially good, we can see.

Reflect

With that said, Proxy must mention Reflect’s new API for ES6. Reflect objects are used to manipulate objects just like Proxy objects, but the purpose of the Reflect object is significant.

  1. Put some methods of Object that are clearly internal to the language (such as Object.defineProperty) on Reflect. At this stage, some methods are deployed on both Object and Reflect objects, and future new methods will only be deployed on Reflect objects. That is, from the Reflect object you can get the methods inside the language. The nice thing about this is that it makes the Object class more pure, JavaScript more like a language, and Object more like a class, rather than a bunch of random methods piled on top of it.

  2. Modify the return results of some Object methods to make them more reasonable. For example, Object.defineProperty(obj, name, desc) throws an error if the attribute cannot be defined, while Reflect.defineProperty(obj, name, desc) returns false.

    // Try {object.defineProperty (target, property, attributes); // success} catch (e) {// failure} // new if (reflect.defineProperty (target, property, attributes)) { // success } else { // failure }Copy the code
  3. Make all Object operations functions. Some Object operations are imperative, such as name in obj and delete obj[name], while reflect.has (obj, name) and reflect.deleteProperty (obj, name) make them functional behavior.

    // Reflect. Has (Object, 'assign') // trueCopy the code
  4. (Core) Reflect object method and Proxy object method one by one, as long as the Proxy object method, you can find the corresponding method on Reflect object. This allows the Proxy object to easily call the corresponding Reflect method, completing the default behavior as a basis for modifying the behavior. That is, no matter how the Proxy changes the default behavior, you can always get the default behavior in Reflect.

There are 13 static methods on the Reflect object.

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

Most of the functions of these methods are the same as those of methods of the same name on Object objects, and they correspond to the methods of Proxy objects one by one.

Let’s rewrite the previous proxy example 🌰 and add Reflect:

Var arr = [1,2] var arrProxy = new Proxy(arr, {get(target, propKey, receiver) {console.log(' getting ${propKey}! `) return Reflect.get(target, propKey, receiver) }, set(target, propKey, value, receiver) { console.log(`setting ${propKey}! ') return reflect. set(target, propKey, receiver)},}) arrProxy[0] = 'change' //setting 0! Change // Read value arrProxy[1] //getting 1! 2Copy the code

In the above code, the corresponding Reflect method is called internally for each Proxy object intercept operation (get, set) to ensure that the native behavior can be executed properly. Another important reason Reflect is called in Proxy is because of the recevier parameter.

Look at 🌰 to show that the recevier parameter is used 👇

let p = {
  a: "a",
}

let handler = {
  set(target, key, value, receiver) {
    console.log("set")
    Reflect.set(target, key, value, receiver)
  },
  defineProperty(target, key, attribute) {
    console.log("defineProperty")
    Reflect.defineProperty(target, key, attribute)
  },
}

let obj = new Proxy(p, handler)
obj.a = "A"
// set
// defineProperty
Copy the code

In the above code, reflect. set is used in the proxy.set interception and passed in the receiver, causing proxy.defineProperty interception to be triggered.

This is because proxy.set’s receiver parameter always points to the current Proxy instance (obj in the above example), whereas reflect.set, once passed to the receiver, assigns properties to the receiver (obj), causing a defineProperty interception to be triggered. If reflect. set is not passed to the receiver, the defineProperty interception is not triggered.

Therefore, the purpose of the receiver is to make all object operations in the Proxy refer to the current Proxy instance, so that all operations on the instance can be intercepted. (Well, very precise :+1:

Contrast the Proxy Object. DefineProperty

By now, I’m sure you understand the difference between Object.defineProperties and Proxy. But again, Proxy is not a syntax sugar for Object.defineproperties !!!!

Babel translates ES6 syntax into ES5 syntax supported by most browsers via @babel/polyfill(corejs and Re-Generator). The principle is that some ES6 features can be replaced with ES5, but writing in ES5 would be cumbersome or unintuitive.

For example, let’s look at a piece of code 👇 :

Class Person{constructor(name){this.name = name} sayName(){console.log(this.name)}}Copy the code

This method defines the class so that it can be recognized by most browsers, we will translate it using Babel:

Function Person(name){this.name = name} person.prototype.sayname = function(){// function Person(name){this.name = name} Person. console.log(this.name) }Copy the code

The translated ES5 code implements exactly the same functionality as the previous ES6 version. We can also say that the way a class defines a class is the syntactic sugar of a class (constructor) defined by function.

However, Proxy cannot be translated through Babel because there is no syntax in ES5 that emulates the features of Proxy. So Vue3. X doesn’t double as some of the older browsers.

Responsive code

function reactive(target = {}) { if (typeof target ! = = "object" | | target = = null) {return target} / / proxy configuration const proxyConf = {get (target, key, Const ownKeys = reflect.ownkeys (target) if (ownkeys.includes (key)) {// If (ownkeys.includes (key)) {// If (ownkeys.includes (key)); Console. log("get", key)} const result = reflect. get(target, key, Return reactive(result)}, set(target, key, val) Receiver) {if (val === target[key]) {return true} const ownKeys = reflect.ownkeys (target); If (ownkeys.includes (key)) {console.log(" existing key", key)} else {console.log(" new key", key) } const result = Reflect.set(target, key, val, receiver) console.log("set", key, Val) return result}, deleteProperty(target, Key) {const result = reflect.deleteProperty (target, key) console.log("delete property", key) return result}, Const observed = new Proxy(target, proxyConf) return observed}Copy the code

How Vue3’s reactive logic is constructed step by step, I put in another blog post Vue3’s reactive implementation logic.

conclusion

Ok, I just want to say something at the end.

There are a lot of people who feel that Vue3 is not worth the cost of abandoning some browsers for performance. But I don’t think so.

First of all, Vue3 is currently in the RA phase and it will take a while before it is released. And Vue3 is not released, we have to use 3 for the project, there should be a long transition period from Vue2. X to 3. As time goes on, I believe more and more browsers will support the Proxy attribute, and we will abandon compatibility with older and older browser versions.

Secondly, the performance improvement of Vue should not be considered because of the incompatibility of some browsers at present. The technology is promoted slowly and iteratively. The Internet would not have developed so fast if it were all guided by the status quo.

Well, that’s it.