Proxy 与 Vue

You Yuxi delivered a keynote speech titled “Vue 3.0 Updates”, elaborating on the update plan and direction of “Vue 3.0” (interested partners can take a look at the complete PPT), indicating that the use of’Object.defineProperty 'has been abandoned in favor of the faster native ’ Proxy '!!

This removes many of the limitations of the previous Vue2.x implementation based on Object.defineProperty: the inability to listen for property additions and deletions, array index and length changes, and support for Map, Set, WeakMap, and WeakSet!

Proxy Introduction

Overview

Proxy is used to modify the default behavior of certain operations, which is equivalent to making modifications at the language level, so it belongs to a kind of “meta programming”, that is, programming in programming languages.

Proxy can be understood as setting up a layer of “interception” before the target object. Access to the object from the outside world must first pass through this layer of interception, so it provides a mechanism to filter and rewrite the access from the outside world. The original meaning of the word proxy is proxy, which is used here to mean that it “proxies” certain operations, which can be translated as “proxy”.

1
2
3
4
5
6
7
8
9
10
var obj = new Proxy ({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});

The above code sets up a layer of interception on an empty object, redefining the behavior of reading (‘get’) and setting (‘set’) properties. The specific syntax will not be explained here for the time being, just look at the running result. Read and write the properties of the object’obj 'with the interception behavior, and you will get the following result.

1
2
3
4
5
6
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2

The above code shows that the proxy actually overloads the dot operator, that is, overwrites the original definition of the language with its own definition.

ES6 natively provides the Proxy constructor function to generate Proxy instances.

1
var proxy = new Proxy(target, handler);

All uses of Proxy objects are in the above form, the only difference is the writing of the’handler ‘parameter. Among them,’ new Proxy () ‘means to generate a’Proxy’ instance, ‘target’ parameter means the target object to be intercepted, and’handler 'parameter is also an object used to customize the interception behavior.

Here is another example of intercepting read property behavior.

1
2
3
4
5
6
7
8
9
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

In the above code, as a constructor function, ‘Proxy’ accepts two parameters. The first parameter is the target object to be proxied (the above example is an empty object), that is, if there is no intervention of’Proxy ‘, the object that the operation originally wants to access; the second parameter is a configuration object. For each proxied operation, a corresponding processing function needs to be provided, which will intercept the corresponding operation. For example, in the above code, the configuration object has a’get’ method to intercept access requests to target object properties. The two arguments to the’get ‘method are the target object and the property to be accessed. As you can see, since the intercepting function always returns’ 35 ‘, accessing any property will result in’ 35 '.

Note that for’Proxy ‘to work, you must operate on the’Proxy’ instance (the’proxy 'object in the above example), not on the target object (the empty object in the above example).

If the’handler 'does not set any interceptions, it is equivalent to going directly to the original object.

1
2
3
4
5
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

In the above code, ‘handler’ is an empty object, without any interception effect, accessing’proxy ‘is equivalent to accessing’target’.

One trick is to set the Proxy object to the’object.proxy ‘property, so that it can be called on the’object’ object.

1
var object = { proxy: new Proxy(target, handler) };

Proxy instances can also serve as prototype objects for other objects.

1
2
3
4
5
6
7
8
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});

let obj = Object.create(proxy);
obj.time // 35

In the above code, the’proxy ‘object is the prototype of the’obj’ object. The’obj ‘object itself does not have the’time’ property, so according to the prototype chain, the property will be read on the’proxy 'object, resulting in interception.

The same interceptor function can be set to intercept multiple operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var handler = {
get: function(target, name) {
if (name = 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},

apply: function(target, thisBinding, args) {
return args[0];
},

construct: function(target, args) {
return {value: args[1]};
}
};

var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype = Object.prototype // true
fproxy.foo = "Hello, foo" // true

For operations that can be set, but do not set interception, they fall directly on the target object and produce results in the original way.

Below is a list of interception operations supported by Proxy, a total of 13.

  • ** get (target, propKey, receiver) **: Intercepts 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’, and 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, returning an array. This method returns the property names of all the properties of the target object itself, while the return result of’Object.keys () ’ only includes the traversable properties of the target object itself.
  • ** getOwnPropertyDescriptor (target, propKey) **: Intercept Object.getOwnPropertyDescriptor (proxy, propKey) and return the description object of the property.
  • ** defineProperty (target, propKey, propDesc) **: Intercept’Object.defineProperty (proxy, propKey, propDesc) ‘,’ Object.defineProperties (proxy, propDescs) ', return a boolean value.
  • ** preventExtensions (target) **: Intercept’Object.preventExtensions (proxy) 'and return a boolean value.
  • ** getPrototypeOf (target) **: Intercept Object.getPrototypeOf (proxy) and return an object.
  • ** isExtensible (target) **: Intercept’Object.isExtensible (proxy) 'and return a boolean value.
  • ** setPrototypeOf (target, proto) **: Intercepts’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 the operation of the proxy instance as a function call, such as’proxy (… args) ‘,’ proxy.call (object,… args) ‘,’ proxy.apply (…) '.
  • ** construct (target, args) **: Intercepts operations called by Proxy instances as constructor functions, such as’new proxy (… args) '.

Advanced usage

get

By using Proxy, the operation of reading properties (‘get’) can be transformed into executing a function, thereby realizing the chain operation of properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var pipe = function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName = 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});

return oproxy;
}

var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63

After the above code sets Proxy, the effect of chaining the function name is achieved.

set

Sometimes, we will set internal properties on the object. The first character of the property name starts with an underscore, indicating that these properties should not be used externally. Combining the’get ‘and’set’ methods can prevent these internal properties from being read and written externally.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] = '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

In the above code, as long as the first character of the property name read and write is an underscore, it will be thrown wrong, so as to achieve the purpose of prohibiting reading and writing internal properties.

has

The’has’ method is used to intercept the’HasProperty ‘operation, that is, it will take effect when determining whether an object has a certain property. A typical operation is the’in’ operator.

The’has’ method can accept two parameters, namely the target object and the property name to be queried.

The following example uses the has method to hide certain properties from the in operator.

1
2
3
4
5
6
7
8
9
10
11
var handler = {
has (target, key) {
if (key[0] = '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

In the above code, if the first character of the property name of the original object is an underscore, ‘proxy.has’ will return’false ‘, so it will not be detected by the’in’ operator.

If the original object is not configurable or prohibits expansion, ‘has’ interception will report an error.

1
2
3
4
5
6
7
8
9
10
var obj = {a: 10};
Object.preventExtensions(obj);

var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});

'a' in p // TypeError is thrown

In the above code, the’obj ‘object prohibits expansion, and as a result, using the’has’ interception will report an error. That is, if a property is not configurable (or the target object is not extensible), the’has’ method must not “hide” (i.e. return’false ') the property of the target object.

It is worth noting that the “has” method intercepts the “HasProperty” operation, not the “HasOwnProperty” operation, that is, the “has” method does not determine whether a property is a property of the object itself or an inherited property.

Use of Proxy in Vue

vue2.x

Recursion traverses the data in the data, using Object.defineProperty()Hijack getters and setters, do data dependency collection processing in getters, and in setters, listen for changes in data and notify the place that subscribes to the current data. 部分源码 src/core/observer/index.js#L156-L193, the version is 2.6.11 as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
let childOb = !shallow && observe(val)
//Perform a deep traversal of the data in the data, adding a response to each property 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 it is an array, you need to perform dependency collection on each member. If the members of the array are still arrays, then recursion.
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 observed again to ensure that the data is responsive
childOb = !shallow && observe(newVal)
Notify all observers of data changes
dep.notify()
}
})

What’s wrong with doing this?

  • Undetectable addition and deletion of object properties: When you add a new property’newProperty ‘to an object, the newly added property does not have a mechanism for vue to detect data updates (because it is added after initialization).’ vue. $set ‘lets vue know that you have added a property, and it will handle it for you.’ $set 'is also handled internally by calling’Object.defineProperty () ’
  • Unable to monitor the change of the index of the array, resulting in setting the value of the array directly through the index of the array, and cannot respond in real time.
  • When there is a lot of data in the data and the hierarchy is very deep, there will be performance issues, because it is necessary to traverse all the data in the data and set it to be responsive.

vue3.0

Vue3.0 has not been officially released yet, butvue-next The relevant code has been open sourced and is currently in an alpha version.

Why use Proxy to solve the above problems? Mainly because Proxy is an interception object, an “interception” is performed on the “object”, and external access to the object must first pass this layer of interception. No matter what properties of the access object, previously defined or newly added, it will go to interception,

Example

Here is a simple data response using Object.defineProperty () and Proxy respectively

Use Object.defineProperty () to implement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Observer {
constructor(data) {
//traverse the properties of the parameter data, add it to this
for(let key of Object.keys(data)) {
if(typeof data[key] = 'object') {
data[key] = new Observer(data[key]);
}
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
Console.log ('you visited' + key);
Return data [key];//The bracketed method can use variables as attribute names, while the dot method cannot;
},
set(newVal) {
Console.log ('You set' + key);
console.log('新的' + key + '=' + newVal);
if(newVal = data[key]) {
return;
}
data[key] = newVal;
}
})
}
}
}

const obj = {
name: 'app',
age: '18',
a: {
b: 1,
c: 2,
},
}
const app = new Observer(obj);
app.age = 20;
console.log(app.age);
app.newPropKey = 'New property';
console.log(app.newPropKey);

The execution result of the above code is

1
2
3
4
5
6
7
//Modify the output of the original attribute age of obj
You set the age
New age = 20
You visited the age
20
//Set the output of the new property
New properties

As you can see, adding a property to an object is not monitored internally. The newly added property needs to be manually monitored again using’Object.defineProperty () ‘. This is why the addition and deletion of object properties cannot be detected in’vue 2.x’. The ‘$set’ provided internally is handled by calling’Object.defineProperty () '.

Next we use’Proxy 'instead of’Object.defineProperty () ’ implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const obj = {
name: 'app',
age: '18',
a: {
b: 1,
c: 2,
},
}
const p = new Proxy(obj, {
get(target, propKey, receiver) {
Console.log ('You visited' + propKey);
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
Console.log ('You set' + propKey);
console.log('新的' + propKey + '=' + value);
Reflect.set(target, propKey, value, receiver);
}
});
p.age = '20';
console.log(p.age);
P.newPropKey = 'New property';
console.log(p.newPropKey);
P.a.d = 'This is an attribute of a in obj';
console.log(p.a.d);
Copy the code

You can see the output below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Modify the age attribute of the original object
You set the age
New age = 20
You visited the age
20

//Set new properties
You set up newPropKey
New newPropKey = new property
You accessed newPropKey
New properties

//Set property d for the a property of obj (which is an object)
You visited a
You visited a
This is the property of a in obj

Reference article:

https://es6.ruanyifeng.com/#docs/proxy

https://juejin.im/post/5e69ee2be51d4527196d6a24