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 as
proxy.foo
和proxy['foo']
. - set(target, propKey, value, receiver): Intercepts the setting of object properties, such as
proxy.foo = v
或proxy['foo'] = v
, returns a Boolean value. - has(target, propKey)Intercept:
propKey in proxy
Returns 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... in
Loop 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 as
proxy(... args)
,proxy.call(object, ... args)
,proxy.apply(...)
. - construct(target, args): intercepts operations called by Proxy instances as constructors, such as
new 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