“This is the fifth day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

I. Agent mode

Proxy

Proxy provides powerful Javascript metaprogramming, and although it is not as common as other ES6 features, Proxy has many features, including operator overloading, object emulation, concise and flexible API creation, object change events, and even the internal response system behind Vue 3 to power it.

Proxy is used to modify the default behavior of certain operations, which can also be understood as a layer of interception in front of the target object. All external access passes through this layer first, so we call it the Proxy mode.

ES6 natively provides a Proxy constructor to generate a Proxy instance.

var proxy = new Proxy(target, handler);
Copy the code

All uses of the Proxy object are in this form, except the way the handle parameter is written. New Proxy is used to generate the Proxy instance, target represents the object to be intercepted, and Handle is used to customize the interception behavior. Example:

const target = {}
const proxy = new Proxy(target, {
    get: (obj, prop) = > {
        console.log('Set get operation')
        return obj[prop];
    },
    set: (obj, prop, value) = > {
        console.log('set operation') obj[prop] = value; }}); proxy.a =2  / / set operation
proxy.a  // Set the get operation
Copy the code

The get and set methods are triggered when a target object is assigned or gets a property, respectively. Get and SET are the proxies we set up to override the default assignment or get behavior. Of course, in addition to get and set, Proxy can intercept a total of 13 other operations

/* handler.get handler.set handler.has handler.apply handler.construct handler.ownKeys handler.deleteProperty handler.defineProperty handler.isExtensible handler.preventExtensions handler.getPrototypeOf handler.setPrototypeOf handler.getOwnPropertyDescriptor */
var target = function (a,b) { 
  return a + b;
 };
const proxy = new Proxy(target, {
    apply: (target, thisArg, argumentsList) = > {
        console.log('apply function', argumentsList)
        return target(argumentsList[0], argumentsList[1]) * 10; }}); proxy(1.2)
Copy the code

The use of the Proxy

Verify the properties

let validator = {
  set: (obj, prop, value) = > {
    if(prop === 'age') {
      if(!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer')}if(value > 200) {
        throw new TypeError('The age is seems invalid')
      }
    }
    obj[prop] = value;

    return true; }};let p = new Proxy({}, validator);
p.age = '11' // Uncaught TypeError: The age is not an integer
p.age = 2000 // Uncaught TypeError: The age is seems invalid
p.age = 18 // true
Copy the code

We may sometimes impose restrictions on certain attributes of an object, such as age, which can only be a string and not more than 200 years old. When these requirements are not met, we can throw an error through the proxy

2. Data-driven: Reactivity

Vue3 data responsive system core is a Proxy Proxy model, we look at the source code, reactivity source location in the packages file, the following simplified source code.

// The code has been deleted
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
// rawToReactive and reactiveToRaw are weakly referenced Map structures
// These two maps are used to store raw data and respondable data
// After Proxy is created, raw data and Proxy objects need to be stored in the two Map structures respectively
const rawToReactive = new WeakMap(a)// The key is the raw data and the value is the response data
const reactiveToRaw = new WeakMap(a)// The key is the response data, and the value is the raw data
// targetMap saves the target object
export const targetMap = new WeakMap<any, KeyToDepMap>()
// entry
function reactive(target) {
 // if trying to observe a readonly proxy, return the readonly version.
 // If the proxy is read-only, return it directly
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  // If the target is marked as read-only by the user, create a read-only Proxy with readonly
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
  )
}

function createReactiveObject(target, toProxy, toRaw, baseHandlers) {
  let observed = toProxy.get(target)
  // The original data already has corresponding responsible data, return responsible data
  if(observed ! = =void 0) {
    return observed
  }
  // The original data is already responsive data
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, baseHandlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  // Save the original data as a key in targetMap. The value is a Map type
  // 
  if(! targetMap.has(target)) { targetMap.set(target,new Map()}return observed
}
Copy the code

Reactive is an exposed entry method that does only one thing: decide whether to create a read-only Proxy object, call ReadOnly to create it, and use createReactiveObject to generate the response data.

The first step in createReactiveObject is to try to get the target’s responsive data in toProxy, and if so, directly return the obtained data. The second step is to determine whether the target’s responsive data is already available. The third step is to create responsible data using the new Proxy, where baseHandlers are defined in the./baseHandlers. Ts file. After creation, save the data to toProxy and toRaw for next creation.

Now that we know how responsive data is created, let’s look at the handler implementation defined in baseHandlers

get

So let’s look at a little bit of code,

let handler = {
  get: (obj, prop) = > {
      console.log('get operation')
      return obj[prop];
  },
  set: (obj, prop, value) = > {
    console.log('set operation')
    return true; }};let p = new Proxy({
  a: {}
}, handler);
p.a.c = 1  / / get operation
Copy the code

In this case, we will assign to the a object in target, but we will not trigger deep data assignment in set, but will trigger get, so there is a problem, the deeper data can not be proxy. The solution is very simple, is to check whether the value is an object through get, if so, then the value through Proxy.

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    const res = Reflect.get(target, key, receiver)
    // The dependency function is saved in the targetMap
    track(target, OperationTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

let handler = {
  get: createGetter(false),
  set: (obj, prop, value) = > {
    console.log('set operation')
    return true; }};let p = new Proxy({
  a: {}
}, handler);
p.a.c = 1  / / get operation
Copy the code

In vue3, the createGetter method is used to return GET. If reflect. get is Object, the createGetter method will call Reactive to generate the Proxy Object. In addition, every proxy data will be saved in WeakMap, which will be directly searched when accessing, thus improving performance. The track method is related to effect, which we will discuss later.

set

function set(target: any, key: string | symbol, value: any, receiver: any) :boolean {
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  // Whether to add a key
  // trigger is used to trigger a callback
  if(! hadKey) { trigger(target, OperationTypes.ADD, key) }else if(value ! == oldValue) { trigger(target, OperationTypes.SET, key) }return result
}
Copy the code

The set function has two main functions, the first is to set a value and the second is to call trigger, which is also in effect. In simple terms, if value.num is used in an effect callback, the callback is collected by the track method and triggered by trigger when value.num = 2 is called.

So how do you collect this content? Which brings us to the targetMap object. TargetMap is a WeakMap type created in Reactive that is used to store dependencies.

// effect.ts
import { targetMap } from './reactive'

// track is used to store callbacks in targetMap
export function track(target: any, type: OperationTypes, key? : string | symbol) {
  if(! shouldTrack) {return
  }
  / / activeReactiveEffectStack remain dependent on the use of function
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  if (effect) {
    // All this function does is plug dependencies into the map to find out if there are dependencies next time
    // Save the effect callback
    // By getting the map-type data saved on targetMap
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      // Nothing, set an empty map to it
      targetMap.set(target, (depsMap = new Map()))}// Get the dependency in target
    let dep = depsMap.get(key!)
    if (dep === void 0) { depsMap.set(key! , (dep =new Set()))}if(! dep.has(effect)) { dep.add(effect) effect.deps.push(dep) } } }Copy the code

Let’s look at the components of effect again

function createReactiveEffect(
  fn: Function,
  options: ReactiveEffectOptions
) :ReactiveEffect {
  // A series of assignment operations, focusing on the implementation of run
  const effect = function effect(. args) :any {
    return run(effect as ReactiveEffect, fn, args)
  } as ReactiveEffect
  effect.isEffect = true
  effect.active = true
  effect.raw = fn
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  // To collect dependent functions
  effect.deps = []
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: any[]) :any {
  if(! effect.active) {returnfn(... args) }if (activeReactiveEffectStack.indexOf(effect) === -1) {
    cleanup(effect)
    // The callback push is executed, and the callback completes the pop
    / / activeReactiveEffectStack remain dependent on the use of function
    // For example:
    // const counter = reactive({ num: 0 })
    // effect(() => {
    // console.log(counter.num)
    // })
    // counter.num = 7
    // The effect callback triggers the get function for counter as it executes
    // The get function triggers track. During the track call, effect.deps. Push (dep) is executed and the track function is called
    // Push the callback onto the deps property of the callback
    Num = 7 the next time counter. Num = 7 the set function of counter is triggered
    // Trigger is triggered by the set function. In trigger, effects.foreach (run) executes any callbacks that need to be executed
    try {
      activeReactiveEffectStack.push(effect)
      returnfn(... args) }finally {
      activeReactiveEffectStack.pop()
    }
  }
}
Copy the code

Why use Proxy instead of defineProperty

  1. definePropertyOnly properties of an object can be hijacked, meaning that you need to traverse the object to hijack each property, whileProxyYou can listen on objects rather than properties, so performance is high
  2. definePropertyCan’t listen on arrays,Proxycan
  3. definePropertyCan only listengetandsetAnd theProxyCan intercept 13 operations
  4. So one way to think about it is,ProxyisObject.definePropertyA full range of enhanced version

The last

Create a Proxy object using createReactiveObject and store it as a key in targetMap. The track function is called when the GET method is triggered, saving the dependent function in targetMap. Trigger runs the callback when the set is triggered.