The introduction
Vue3.0 Base has finally been released. Recently, we looked at the related functionality and API documentation, where the responsiveness was changed from object.defineProperty () to Proxy Proxy.
What is a Proxy
What is a Proxy? In a nutshell, it adds a layer of interception on the original object. When the outside world accesses the object, it must pass through this layer object. Thus a layer of mechanisms is provided to filter and modify access to external objects.
- Get (target, propKey, receiver): Intercepts the reading of objects.
- Set (Target, propKey, receiver): Settings for intercepting objects.
- Has (target, propKey): Intercepts the propKey in the proxy, returning 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 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 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)
.
usage
var person = {
name: "Zhang"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist."); }}}); proxy.name// "/"
proxy.age // Throw an error
Copy the code
The above quote is from Nguyen Yifeng’s introduction to ECMAScript 6
Compare Proxy with Object.defineProperty
The Proxy advantages
- Proxies can support arrays natively, while Object.defineProperty does not support array operations
- Proxy can monitor delete new attributes, while Object.defineProperty can only monitor GET and set attributes
- Proxy has 13 intercepting methods, but Object.defineProperty does not
The Proxy shortcomings
- Proxy has compatibility issues, not supported for IE11, Object. DefineProperty does not have compatibility issues
Vue3.0 responsive implementation
- Reactive is used to create proxy agents
- Effect generates a reactive expression
1. Create proxy objects by reactive
Reactive function
- The target parameter specifies the object to be proxied
- Return value: proxied object
function reactive(targe) {
// If not an object, no proxy is required
if(! isObject(target)) {return;
}
return createReactiveObject(target);
}
// The function that actually implements the proxy
function createReactiveObject(target) {
const baseHandle = {
get(target, key, receive) {
console.log('get', key)
const res = Reflect.get(target, key, receive);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receive){
const hasKey = hasOwn(target, key); // Check whether the attribute exists
const oldValue = target[key];
const res = Reflect.set(target, key, value, receive);
return res; // There must be a return value. If there is no return value, an error will be reported when the array is proided
},
deleteProperty(target, key){
const res = Reflect.deleteProperty(target, key);
returnres; }}const observer = new Proxy(target, baseHandle);
toProxy.set(target, observer);
toRaw.set(observer, target);
return observer;
}
// demo
const person = {name: 'dragon'.age: 20};
const proxy = reactive(person);
proxy.age = 21;
console.log(proxy.age) / / 21
Copy the code
The object is proxied at this point, but there are some problems. When an object is proxied more than once, multiple objects are generated:
const person = {name: 'dragon'.age: 20};
const proxy = reactive(person);
const proxy1 = reactive(person);
const proxy2 = reactive(proxy1);
// The object can be proxied multiple times, and the proxied object can be proxied twice
Copy the code
Modify the scheme, you can make a cache, save the corresponding relationship between the object and the object of the proxy:
+ const toProxy = new WeakMap(a);// Add key and proxy objects
+ const toRaw = new WeakMap(a);// Put a proxy object and a wish object.
// Modify the reactive function
function reactive(target) {
if(! isObject(target)) {return;
}
// Prevent multiple proxies
+ const observer = toProxy.get(target);
+ if(observer) {
+ returnobserver; +}// Prevent multiple layers of agents
+ if(toRaw.has(target)) {
+ returntarget; +}return createReactiveObject(target);
}
Copy the code
2. Collect through the effect function
// When proxy.name is modified, a callback is invoked first
effect((a)= > {console.log(proxy.name)})
Copy the code
Effect to realize
activeEffectStacks = []; // Callback function stack
// response. Side effects
function effect(fn) {
const effect = createReactiveEffect(fn);
effect();
}
// Create a reactive effect
function createReactiveEffect(fn) {
const effect = function() {
return run(effect, fn); // let fn execute and effect stack
}
return effect;
}
function run(effect, fn) { // Run fn and stack effect
try{
activeEffectStacks.push(effect);
fn(); // fun calls values when executing
}finally{ activeEffectStacks.pop(); }}Copy the code
Modify the get and set methods in baseHandle to collect data
/ / modify createReactiveObject
function createReactiveObject(target) {
const baseHandle = {
get(target, key, receive) {
console.log('get', key)
const res = Reflect.get(target, key, receive);
// Determine if the current returned value is an object, if so, re-proxy
+ track(target, key); // Collect dependencies
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receive){
const hasKey = hasOwn(target, key); // Check whether the attribute exists
const oldValue = target[key];
const res = Reflect.set(target, key, value, receive);
+ if(! hasKey) {// Add attributes
+ trigger(target, 'add', key); +}else if(oldValue ! == value) {/ / update
+ trigger(target, 'set', key); +}return res; // There must be a return value. If there is no return value, an error will be reported when the array is proided
},
deleteProperty(target, key){
const res = Reflect.deleteProperty(target, key);
returnres; }}const observer = new Proxy(target, baseHandle);
toProxy.set(target, observer);
toRaw.set(observer, target);
return observer;
}
Copy the code
Track and trigger methods are added to this process, where track collects and trigger methods are called.
let targetsMap = new WeakMap(a);function track(target, key) { // If the key in the target changes, I execute the method
const effect = activeEffectStacks[activeEffectStacks.length - 1];
if(effect) {
let depsMap = targetsMap.get(target);
if(! depsMap) { targetsMap.set(target, (depsMap =new Map()));
}
let deps = depsMap.get(key);
if(! deps) { depsMap.set(key, (deps =new Set()));
}
if(! deps.has(effect)) {console.log(effect); deps.add(effect); }}}function trigger(target, type, key) {
let depsMap = targetsMap.get(target);
if(depsMap) {
const deps = depsMap.get(key);
if(deps) {
deps.forEach(effect= >{ effect(); }); }}}Copy the code
The overall process of collection
- Call the effect method, createReactiveEffect() in the effect method generates the actual callback function effect, while calling effect;
- When effect is called, the run method is fired. At this point, effect, the callback function, is added to the stack and fn is called from run
- When fn is called, the get method will be triggered, which will be collected through track in GET and cache the results to
targetsMap
In the - When set is called, the response is triggered in trigger
The complete code
const toProxy = new WeakMap(a);// Add key and proxy objects
const toRaw = new WeakMap(a);// Put a proxy object and a wish object.
// Determine whether it is an object
function isObject(target) {
return typeof target === 'object'&& target ! = =null;
}
// Check whether key is included
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}
function reactive(target) {
if(! isObject(target)) {return;
}
// Prevent multiple proxies
const observer = toProxy.get(target);
if(observer) {
return observer;
}
// Prevent multiple layers of agents
if(toRaw.has(target)) {
return target;
}
return createReactiveObject(target);
}
// Create a responsive object
/** * Refelct will return a value, no error, can be traversed symbol */
function createReactiveObject(target) {
const baseHandle = {
get(target, key, receive) {
console.log('get', key)
const res = Reflect.get(target, key, receive);
// Determine if the current returned value is an object, if so, re-proxy
track(target, key); // Collect dependencies
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receive){
const hasKey = hasOwn(target, key); // Check whether the attribute exists
const oldValue = target[key];
const res = Reflect.set(target, key, value, receive);
if(! hasKey) {// Add attributes
trigger(target, 'add', key);
} else if(oldValue ! == value) {/ / update
trigger(target, 'set', key);
}
return res; // There must be a return value. If there is no return value, an error will be reported when the array is proided
},
deleteProperty(target, key){
const res = Reflect.deleteProperty(target, key);
returnres; }}const observer = new Proxy(target, baseHandle);
toProxy.set(target, observer);
toRaw.set(observer, target);
return observer;
}
let activeEffectStacks = [];
let targetsMap = new WeakMap(a);function track(target, key) { // If the key in the target changes, I execute the method
const effect = activeEffectStacks[activeEffectStacks.length - 1];
if(effect) {
let depsMap = targetsMap.get(target);
if(! depsMap) { targetsMap.set(target, (depsMap =new Map()));
}
let deps = depsMap.get(key);
if(! deps) { depsMap.set(key, (deps =new Set()));
}
if(! deps.has(effect)) {console.log(effect); deps.add(effect); }}}function trigger(target, type, key) {
let depsMap = targetsMap.get(target);
if(depsMap) {
const deps = depsMap.get(key);
if(deps) {
deps.forEach(effect= >{ effect(); }); }}}// response. Side effects
function effect(fn) {
const effect = createReactiveEffect(fn);
effect();
}
// Create a reactive effect
function createReactiveEffect(fn) {
const effect = function() {
return run(effect, fn); // let fn execute and effect stack
}
return effect;
}
function run(effect, fn) { // Run fn and stack effect
try{
activeEffectStacks.push(effect);
fn(); // fun calls values when executing
}finally{ activeEffectStacks.pop(); }}// Rely on collect, publish and subscribe
const proxy = reactive({name: {n: 1}});
effect((a)= > {
console.log(proxy.name.n)
})
proxy.name.n = 3;
Copy the code