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... 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).

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

  1. Call the effect method, createReactiveEffect() in the effect method generates the actual callback function effect, while calling effect;
  2. 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
  3. When fn is called, the get method will be triggered, which will be collected through track in GET and cache the results totargetsMapIn the
  4. 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