Understand object Defineproperty and Proxy, vue3 Why does 0 use Proxy?

Posted by norbie on Thu, 03 Mar 2022 05:27:36 +0100

preface

During the interview a few days ago, the interviewer asked this question and felt that he didn't answer it very well. I'll sort it out here~
The length of the article will be relatively long, but after reading it, you will gain a lot ~ I hope you keep reading it~

Object.defineProperty()

Function: define a new property on an object, or modify the existing property of an object and return the object.

1. Basic use

Syntax: object defineProperty(obj, prop, descriptor)

Parameters:

  1. Object to add attribute
  2. The name of the attribute to be defined or modified or [Symbol]
  3. The property descriptor to define or modify

Look at a simple example

let person = {}
let personName = 'lihua'
//Add the attribute name on the person object with the value of personName
Object.defineProperty(person, 'name', {
  //However, the default is non enumerable (for in cannot be printed out), but: enumerable: true
  //The default value cannot be modified. It can be: wirtable: true
  //It cannot be deleted by default. configurable: true
    get: function () {
        return personName
    },
    set: function (val) {
        return name=val
    }
})
//When the name attribute of the person object is read, the get method is triggered
console.log(person.name)
//When the name attribute of the person object is modified, the set method is triggered
personName = 'liming'
//After checking, it was found that the modification was successful
console.log(person.name)
Copy code

In this way, we successfully monitored the change of name attribute on person.

2. Listen for multiple attributes on the object

In the above use, we only monitor the changes of one attribute, but in practice, we usually need to monitor the changes of multiple attributes at one time.
At this time, we need to cooperate with object Keys (obj) for traversal. This method can return a character array composed of all enumerable attributes on the obj object. (in fact, you can use for in to traverse.)
Here is a simple usage effect of the API:

var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
Copy code

Using this API, we can traverse all the properties of the hijacked object. However, if we simply combine the above ideas with this API, we will find that it can not achieve the effect. Here is a wrong version I wrote:

Object.keys(person).forEach(function (key) {
    Object.defineProperty(person, key, {
        enumerable: true,
        configurable: true,
        // this is passed in by default
        get() {
            return person[key]
        },
        set(val) {
            console.log(`yes person Medium ${key}Property has been modified`)
            person[key] = val
            // After modification, you can perform rendering operations
        }
    })
})
console.log(person.age)
Copy code

It seems that there is no error in the above code, but try to run it ~ you will overflow the stack like me. Why? Let's focus on the get method. When we access the properties of person, we will trigger the get method and return person[key], but accessing person[key] will also trigger the get method, resulting in recursive call and eventually stack overflow.
This also leads to our following method. We need to set a transit observer so that the return value in get does not directly access obj[key].

let person = {
    name: '',
    age: 0
}
// Implement a responsive function
function defineProperty(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`Visited ${key}attribute`)
            return val
        },
        set(newVal) {
            console.log(`${key}Property is modified to ${newVal}Yes`)
            val = newVal
        }
    })
}
// Implement a traversal function Observer
function Observer(obj) {
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key])
    })
}
Observer(person)
console.log(person.age)
person.age = 18
console.log(person.age)
Copy code

3. Deeply monitor an object

So how do we solve the problem of nesting a pair of objects in an object? In fact, on the basis of the above code, plus a recursion, you can easily implement it~
We can observe that, in fact, observer is the listening function we want to implement. Our expected goal is: as long as we pass an object into it, we can monitor the properties of the object, even if the properties of the object are also an object.
We add a recursive case in the defineProperty() function:

function defineProperty(obj, key, val) {
    //If an object enters an attribute recursively, it is also monitored by the object
    if(typeof val === 'object'){
    observer(val)
    }
    Object.defineProperty(obj, key, {
        get() {
            console.log(`Visited ${key}attribute`)
            return val
        },
        set(newVal) {
            console.log(`${key}Property is modified to ${newVal}Yes`)
            val = newVal
        }
    })
}
Copy code

Of course, we also need to add a recursive stop condition in observer:

function Observer(obj) {
    //If an object is not passed in, return
    if (typeof obj !== "object" || obj === null) {
        return
    }
    // for (key in obj) {
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key])
    })
    // }

}
Copy code

In fact, it's almost solved here, but there's another small problem. When modifying an attribute, if the original attribute value is a string, but we reassign an object, how do we listen to all the attributes of the newly added object? In fact, it is also very simple. You only need to modify the set function:

set(newVal) {
    // If the object is a new recursive object, the Val will listen
    if(typeof val === 'object'){
        observer(key)
    }
    console.log(`${key}Property is modified to ${newVal}Yes`)
    val = newVal
        }
Copy code

Here we are~

4. Listening array

What if the attribute of the object is an array? How do we implement monitoring? See the following code:

let arr = [1, 2, 3]
let obj = {}
//Listen for arr as obj attribute
Object.defineProperty(obj, 'arr', {
    get() {
        console.log('get arr')
        return arr
    },
    set(newVal) {
        console.log('set', newVal)
        arr = newVal
    }
})
console.log(obj.arr)//Output get arr [1,2,3] normal
obj.arr = [1, 2, 3, 4] //Output set [1,2,3,4] is normal
obj.arr.push(3) //Abnormal output of get arr, unable to listen to push
 Copy code

We find that the set method cannot listen to the elements added to the array through the push method.

In fact, to access or modify the existing elements in the array through the index, you can start to get and set, but for the elements added through push and unshift, an index will be added. In this case, you need to initialize manually before the newly added elements can be monitored. In addition, deleting an element through pop or shift will delete and update the index, and trigger setter and getter methods.

In vue2 In X, this problem is solved by rewriting the method on the Array prototype. I won't expand here. Interested UUS can learn more about it~

Proxy

Does it feel a little complicated? In fact, in the above description, we still have a problem to solve: that is, when we want to add a new attribute to an object, we also need to manually listen to the new attribute.

It is precisely for this reason that when using vue to add attributes to arrays or objects in data, you need to use VM$ Set to ensure that the new attribute is also responsive.

As you can see, through object Defineporperty() is troublesome for data monitoring and requires a lot of manual processing. That's why in vue3 Youyuxi in 0 adopts Proxy instead. Next, let's take a look at how Proxy solves these problems~

1. Basic use

Syntax: const p = new Proxy(target, handler) parameter:

  1. Target: the target object to be wrapped by Proxy (it can be any type of object, including native array, function, or even another Proxy)
  2. handler: an object that usually takes a function as an attribute. The functions in each attribute define the behavior of processing p when performing various operations.

Through Proxy, we can intercept some operations on the object for which the Proxy is set. Various external operations on this object must be intercepted through this layer first. (similar to defineProperty)

Let's start with a simple example

//Define an object that needs to be represented
let person = {
    age: 0,
    school: 'Xidian'
}
//Define handler object
let hander = {
    get(obj, key) {
        // If there is this attribute in the object, the attribute value is returned. If not, the default value is returned
        return key in obj ? obj[key] : 66
    },
    set(obj, key, val) {
        obj[key] = val
        return true
    }
}
//Pass the handler object into the Proxy
let proxyObj = new Proxy(person, hander)

// Test whether get can be intercepted successfully
console.log(proxyObj.age)//Output 0
console.log(proxyObj.school)//Output XD
console.log(proxyObj.name)//Output defaults 66

// Test whether the set can be intercepted successfully
proxyObj.age = 18
console.log(proxyObj.age)//Output 18 modified successfully
 Copy code

It can be seen that the Proxy proxy is the whole object, not a specific attribute of the object. We don't need to bind data one by one through traversal.

It is worth noting that we used object After defineproperty () adds a property to the object, our reading and writing operations on the object property are still in the object itself.
However, once we use Proxy, if we want the read-write operation to take effect, we need to operate on the Proxy instance object proxyObj.

In addition, the MDN clearly indicates that the set() method should return a Boolean value, otherwise an error TypeError will be reported.

2. Easily solve the problem of object Problems encountered in defineproperty

Use object When defining property, the problems we encounter are:

1. You can only listen to one attribute at a time. You need to traverse to listen to all attributes. We have solved this above.
2. When the attribute of an object is still an object, recursive listening is required.
3. You need to listen manually for the new attributes of the object
4. You cannot listen to the elements added by the push and unshift methods of the array

These problems are easily solved in Proxy. Let's take a look at the following code.

Check the second question

Based on the above code, we make the structure of the object more complex.

let person = {
    age: 0,
    school: 'Xidian',
    children: {
        name: 'Xiao Ming'
    }
}
let hander = {
    get(obj, key) {
        return key in obj ? obj[key] : 66
    }, set(obj, key, val) {
        obj[key] = val
        return true
    }
}
let proxyObj = new Proxy(person, hander)

// Test get
console.log(proxyObj.children.name)//Output: Xiao Ming
console.log(proxyObj.children.height)//Output: undefined
// Test set
proxyObj.children.name = 'Cai Cai'
console.log(proxyObj.children.name)//Output: Vegetables
 Copy code

You can see that the name attribute of the children object is successfully monitored (as for why children.height is undefined, you can discuss it again)

Test the third question

In fact, this has been mentioned in the basic use. Visit proxyobj Name is a property that does not exist on the original object, but when we access it, we can still be intercepted by get.

Test the fourth question

let subject = ['High number']
let handler = {
    get(obj, key) {
        return key in obj ? obj[key] : 'There is no such subject'
    }, set(obj, key, val) {
        obj[key] = val
        //When the set method succeeds, it should return true, otherwise an error will be reported
        return true
    }
}

let proxyObj = new Proxy(subject, handler)

// Verify get and set
console.log(proxyObj)//Output ['high number']
console.log(proxyObj[1])//I don't have this subject
proxyObj[0] = 'College Physics'
console.log(proxyObj)//Output ['College Physics']

// //Verify whether the elements added by push can be monitored
proxyObj.push('linear algebra')
console.log(proxyObj)//Output ['College Physics',' linear algebra ']
Copy code

So far, our previous problems have been perfectly solved.

3.Proxy supports 13 kinds of interception operations

In addition to get and set to intercept read and assignment operations, Proxy also supports interception of a variety of other behaviors. The following is a brief introduction. If you want to have an in-depth understanding, you can go to MDN.

  • get(target, propKey, receiver): intercept the reading of object attributes, 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 loop, returning an array. This method returns the property names of all its own properties of the target object, while object The returned result of keys () only includes 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. Define properties (proxy, propdescs) and return a Boolean value.

  • preventExtensions(target): intercept object Preventextensions (proxy), which returns a Boolean value.

  • getPrototypeOf(target): intercept object Getprototypeof (proxy), return 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.

  • Call Proxy (. Args) and target (. Args) as the operation of Proxy call(object, ...args),proxy.apply(...).

  • construct(target, args): intercept the operation called by Proxy instance as constructor, such as new proxy(...args).

4. this problem in proxy

Although the Proxy completes the Proxy of the target object, it is not a transparent Proxy, that is, even if the handler is an empty object (i.e. does not act as any Proxy), this in the object he proxies is not the object, but the proxyObj object. Let's take an example:

let target = {
    m() {
        // Check whether this points to proxyObkj
        console.log(this === proxyObj)
    }
}
let handler = {}
let proxyObj = new Proxy(target, handler)

proxyObj.m()//Output: true
target.m()//Output: false
 Copy code

You can see that this inside the proxy object target points to proxyObj. This kind of direction sometimes leads to problems. Let's take a look at the following example:

const _name = new WeakMap();
class Person {
   //Store the name of person in_ On the name attribute of name
  constructor(name) {
    _name.set(this, name);
  }
  //When getting the name attribute of person, return_ Name of name
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxyObj = new Proxy(jane, {});
proxyObj.name // undefined
 Copy code

In the above example, because the name attribute of the jane object depends on the point of this, which points to proxyObj, the proxy cannot work normally.

In addition, the internal properties of some js built-in objects can only be obtained by the correct this, so the Proxy cannot Proxy the properties of these native objects. Take the following example:

const target = new Date();
const handler = {};
const proxyObj = new Proxy(target, handler);

proxyObj.getDate();
// TypeError: this is not a Date object.
Copy code

You can see that an error is thrown when accessing the getDate method in the Date object through the proxy proxy. This is because the getDate method can only be obtained on the Date object instance. If this is not a Date object instance, an error will be reported. So how do we solve this problem? Just manually bind this to the Date object instance. See the following example:

const target = new Date('2015-01-01');
const handler = {
    get(target, prop) {
        if (prop === 'getDate') {
            return target.getDate.bind(target);
        }
        return Reflect.get(target, prop);
    }
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
 Copy code

Sprinkle flowers at the end

So far, my summary is over ~ the article is not very comprehensive, and there are many places not mentioned, such as:

  1. Proxy is often used with Reflect
  2. We often do The create () method adds the Proxy instance Object to the prototype Object of the Object, so that we can directly Object Proxyobj
  3. If you are interested in uu, you can add output to get and set of Proxy. You will find that when we call the push method, get and set will output twice respectively. Why?

There is no end to learning. Let's work together~

If there are mistakes, please correct them

Reference article:

1.Proxy and object Defineproperty introduction and comparison

2.MDN Proxy


Author: Cauliflower
Link: https://juejin.cn/post/7069397770766909476
Source: rare earth Nuggets
 

Topics: Javascript Front-end Vue Interview