As we all know, JS is in the position of front-end development. It’s really important to learn it well.

In the following article, we will introduce Proxy and Reflect. Then we will analyze the principle of vUE responsiveness and guide you to write vUE responsiveness.

Proxy

Listening to the object

Let’s start with a requirement: We have an object in which we want to listen as properties are set or obtained

We can listen for property operations through the stored property descriptor of Object.defineProperty.

    Object.keys(obj).forEach(key= > {
      let value = obj[key]

      Object.defineProperty(obj, key, {
        get: function() {
          console.log('listening on the obj object${key}Property is accessed)
          return value
        },
        set: function(newValue) {
          console.log('listening on the obj object${key}Property is set to the value ')
          value = newValue
        }
      })
    })
Copy the code

But what’s the downside?

  • First of all,bject.definePropertyIt is not designed to listen on all properties of an object. When we define some properties, we originally intended to define common properties, but later we forced it into a data property descriptor.
  • Second, if we want to listen for richer operations, such as adding properties, deleting properties, thenObject.definePropertyThere is nothing that can be done.

Basic Use of Proxy

In ES6, a new Proxy class has been added to help us create a Proxy, as the name suggests. That is, if we want to listen for operations on an object, we can create a Proxy object.

All subsequent operations on the object are done through the proxy object, which listens for what we want to do with the original object.

We can implement the listening object case as a Proxy once:

  • First, we need the new Proxy object and pass in the object we want to listen to and a handler, which we can call a handler.
  • Second, we’re going to operate directly on the Proxy, not the original object, because we need to listen inside the handler.
    const objProxy = new Proxy(obj, {
      // Get the value of the capture
      get: function(target, key) {
        console.log('listening to the object${key}Property is accessed, target)
        return target[key]
      },

      // Set the value of the capture
      set: function(target, key, newValue) {
        console.log('listening to the object${key}Property is set to the value ', target)
        target[key] = newValue
      }
    })
Copy the code

Proxy All captures

What do 13 catchers do?

  • handler.getPrototypeOf():
    • Object.getPrototypeOfMethod.
  • handler.setPrototypeOf():
    • Object.setPrototypeOfMethod.
  • handler.isExtensible():
    • Object.isExtensibleMethod.
  • handler.preventExtensions():
    • Object.preventExtensionsMethod.
  • handler.getOwnPropertyDescriptor()
    • Object.getOwnPropertyDescriptorMethod.
  • handler.defineProperty()
    • Object.definePropertyMethod.
  • handler.ownKeys()
    • Object.getOwnPropertyNamesmethods
    • Object.getOwnPropertySymbolsMethod.
  • handler.has()
    • inOperator.
  • handler.get()
    • The catcher for the property read operation.
  • handler.set()
    • Property set the catcher for the operation.
  • handler.deleteProperty()
    • deleteOperator.
  • handler.apply()
    • A catcher for a function call operation.
  • handler.construct()
    • newOperator.

In order for the proxy object to capture these operations, they are operations on the proxy object. Instead of directly manipulating the original object.

The latter two types of listener are special in that they listen for function objects. Let’s introduce it.

    function foo() {}const fooProxy = new Proxy(foo, {
      apply: function(target, thisArg, argArray) {
        console.log("Apply to foo.")
        return target.apply(thisArg, argArray)
      },
      construct: function(target, argArray, newTarget) {
        console.log("New call to foo")
        return newtarget(... argArray) } }) fooProxy.apply({}, ["zh"."llm"])
    new fooProxy("zh"."llm")
Copy the code

Where set, GET, and receiver represent the proxy object itself. It is usually used in conjunction with reflect. get/set methods to change the getter in the original object, and this in the setter.

    const obj = {
      name: "zh".age: 22
    }

    const objProxy = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(receiver === objProxy) // true
        return Reflect.get(target, key, receiver)
      },
      set: function(target, key, newValue, receiver) {
        const result = Reflect.set(target, key, newValue, receiver)
      }
    })
Copy the code

Reflect

Reflect, also a new API for ES6, is an object that literally means reflection.

So what does Reflect do?

  • It mainly provides a number of methods for manipulating JavaScript objects, somewhat like the methods used to manipulate objects in Object.
  • For example reflect.getPrototypeof (target) is similar to Object.getPrototypeof ().
  • For example reflect.defineProperty (target, propertyKey, Attributes) is similar to Object.defineProperty().

If we have Object to do this, why do we need a new Object like Reflect?

  • This is because earlier ECMA specifications did not consider how such operations on objects themselves would be designed to be more formal, so they were placed on top of objects.
  • But Object is a constructor, and these operations don’t really fit on it.
  • It also contains operators like in and delete, which makes JS look a bit weird.

So in ES6, we added reflectations to focus all of our actions on the Reflect object.

The API relationship between Object and Reflect can be found in the MDN documentation.

All the methods in Reflect

There are 13 proxies, one to one.

  • Reflect.getPrototypeOf(target)

    • Similar to Object.getProtoTypeof ().
  • Reflect.setPrototypeOf(target, prototype)

    • A function that sets the object prototype. Returns a Boolean, true if the update succeeded.
  • Reflect.isExtensible(target)

    • Similar to the Object. IsExtensible ()
  • Reflect.preventExtensions(target)

    • Similar to object.preventExtensions (). Return a Boolean.
  • Reflect.getOwnPropertyDescriptor(target, propertyKey)

    • Similar to the Object. GetOwnPropertyDescriptor (). Returns the property descriptor if the property exists in the object, otherwise undefined.
  • Reflect.defineProperty(target, propertyKey, attributes)

    • Similar to Object.defineProperty(). Returns true on success
  • Reflect.ownKeys(target)

    • Returns an array containing all of its own properties (excluding inherited properties). Keys () similar to Object.keys() but not affected by Enumerable.
  • Reflect.has(target, propertyKey)

    • Determining whether an object has a property is exactly the same as the in operator.
  • Reflect.get(target, propertyKey[, receiver])

    • Gets the value of an attribute on the object, similar to target[name].
  • Reflect.set(target, propertyKey, value[, receiver])

    • A function that assigns values to attributes. Returns a Boolean, true if the update succeeded.
  • Reflect.deleteProperty(target, propertyKey)

    • The delete operator of the function is equivalent to the delete target[name].
  • Reflect.apply(target, thisArgument, argumentsList)

    • Call a function and pass in an array as the call parameter. This is similar to function.prototype.apply ().
  • Reflect.construct(target, argumentsList[, newTarget])

    • New to the constructor is equivalent to executing a new target(… The args).

Let’s take a look at the last method

Its main function is to create an object of class newTarget by calling a constructor of class Target. That’s weird. The second argument, argumentsList, represents the argument to the constructor.

    function Student(name, age) {
      this.name = name
      this.age = age
    }

    function Teacher() {}// Execute the content of the Student function, but create the Teacher object
    const teacher = Reflect.construct(Student, ["zh".22], Teacher)
    console.log(teacher) //Teacher { name: 'zh', age: 22 }
    console.log(teacher.__proto__ === Teacher.prototype) // true
Copy the code

The basic use of Reflect

Here’s how Reflect works with Proxy

    const obj = {
      name: "zh".age: 22
    }

    const objProxy = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log("get---------")
        return Reflect.get(target, key)
      },
      set: function(target, key, newValue, receiver) {
        console.log("set---------")
        const result = Reflect.set(target, key, newValue)
      }
    })

    objProxy.name = "llm"
    console.log(objProxy.name)
Copy the code

The role of the Receiver

The last parameter of handler.set/get in Proxy above is receiver. Seemingly useless, represents the proxy object itself. But he comes into play with Reflect. Reflect modifies the getter on the original object, the this reference on setter calls.

    const obj = {
      _name: "zh".get name() {
        // This points to obj by default, but with reflect.get this points to obj
        return this._name
      },
      set name(newValue) {
        this._name = newValue
      }
    }

    const objProxy = new Proxy(obj, {
      get: function(target, key, receiver) {
        // Receiver is the created proxy object
        console.log("Get method is accessed --------")
        return Reflect.get(target, key, receiver)
      },
      set: function(target, key, newValue, receiver) {
        console.log("Set method is accessed --------")
        Reflect.set(target, key, newValue, receiver)
      }
    })


    objProxy.name = "llm"
    console.log(objProxy.name)
Copy the code

Vue responsive principle

With that in mind, let’s take a look at how the vue(2,3) response is implemented.

What is reactive?

So let’s first look at what does reactive mean?

Let’s look at a piece of code:

  • M has an initialized value, and there is a piece of code that uses this value.
  • This code can be automatically reexecuted when m has a new value.
    let m = 100
    // a piece of code
    console.log(m)
    console.log(m * 2)
    
    m = 200
Copy the code

A code mechanism that automatically responds to data variables is called reactive.

Responsive design

First, there may be more than one line of code executed, so we can put this code into a function. The problem then becomes to automatically execute a function when the data changes.

But there’s a problem: we have a lot of functions in development, so how do we distinguish between a function that needs to be reactive and one that doesn’t?

Functions are needed to collect dependent functions. And store it in an array. When the relevant data changes, the collected dependency functions are executed in turn.

    let reactiveFns = []
    function watchFn(fn) {
      reactiveFns.push(fn)
    }
    
    // When the relevant data changes, the collected dependency functions are executed in turn.
    // The responsivity of the object
    const obj = {
      name: "zh".age: 22
    }

    watchFn(function() {
      const newName = obj.name
      console.log("Hello World")
      console.log(obj.name) / / 100 rows
    })

    watchFn(function() {
      console.log(obj.name, "demo function -------")})function bar() {
      console.log("Ordinary other functions")
      console.log("This function doesn't have to have any response.")
    }

    obj.name = "llm"
    reactiveFns.forEach(fn= > {
      fn()
    })
Copy the code

You can see that from above

  • Any function passed to watchFn needs to be reactive.
  • Other functions defined by default do not require responsiveness.

Collection of reactive dependencies

Since the dependencies we collected are stored in an array, there are data management issues:

  • In real development, we need to listen for responses from many objects.
  • These objects need to listen for more than one property, and many of their property changes have corresponding reactive functions.
  • There is no way to maintain a bunch of arrays globally to hold these response functions.
  • So we’re going to design a class that manages all the reactive functions for a property of an object. An array that replaces the original simple reactiveFns.
    class Depend {
      constructor() {
        this.reactiveFns = []
      }

      // Collect dependencies
      addDepend(reactiveFn) {
        this.reactiveFns.push(reactiveFn)
      }
      / / response
      notify() {
        this.reactiveFns.forEach(fn= > {
          fn()
        })
      }
    }

    // Encapsulate a reactive function
    const depend = new Depend()
    function watchFn(fn) {
      depend.addDepend(fn)
    }

    // The responsivity of the object
    const obj = {
      name: "zh"./ / object depend
      age: 22 / / object depend
    }

    watchFn(function() {
      const newName = obj.name
      console.log("Hello World")
      console.log(obj.name) / / 100 rows
    })

    watchFn(function() {
      console.log(obj.name, "demo function -------")
    })

    obj.name = "llm"
    depend.notify()
Copy the code

The implementation of the above example is still manually triggered. So how do you make it automatically respond to dependency changes?

We need Proxy(vue3) and Object.defineProperty(vue2) to listen for Object changes.

    const objProxy = new Proxy(obj, {
      get: function(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set: function(target, key, newValue, receiver) {
        Reflect.set(target, key, newValue, receiver)
        // Automatically execute when object properties change
        depend.notify()
      }
    })
Copy the code

Dependency management of objects

Up to this point in the design, it still has a lot of flaws

  • Since we have many objects in the project, we need to differentiate between different objects for dependency collection.
  • And for the same object, we also need to collect input dependencies for different attributes.

How do you use one data structure to manage the different dependencies of different objects?

    // Encapsulate a function that gets depend
    const targetMap = new WeakMap(a)function getDepend(target, key) {
      // The process of getting a map based on the target object
      let map = targetMap.get(target)
      if(! map) { map =new Map()
        targetMap.set(target, map)
      }

      // Get the Depend object by key
      let depend = map.get(key)
      if(! depend) { depend =new Depend()
        map.set(key, depend)
      }
      return depend
    }
Copy the code

The previous place we collected dependencies was in watchFn:

  • This way we don’t know which key and which depend need to collect dependencies.
  • You can only add dependent objects to a single Depend object.

So where is the right place to collect it?

It should be when we invoke the Proxy’s GET catcher. Because if an object’s key is used in a function, it should be collected.

    // Encapsulate a reactive function
    let activeReactiveFn = null
    function watchFn(fn) {
      activeReactiveFn = fn
      fn()
      activeReactiveFn = null
    }
    
    const objProxy = new Proxy(obj, {
      get: function(target, key, receiver) {
        // Get the corresponding depend based on target.key
        const depend = getDepend(target, key)
        // Add a response function to depend
        depend.addDepend(activeReactiveFn)

        return Reflect.get(target, key, receiver)
      },
      set: function(target, key, newValue, receiver) {
        Reflect.set(target, key, newValue, receiver)
        // depend.notify()
        const depend = getDepend(target, key)
        depend.notify()
      }
    })
Copy the code

In the above code, reactive functions are defined to collect actions that need to be performed after a dependency change, so a global variable is defined. And inject the dependency function in the Proxy’s handler.get.

Depend refactoring

But there are two problems:

  • If the key is used twice in a function, such as name, the function is collected twice.
    // The passed function is added twice
    watchFn(() = > {
      console.log(objProxy.name, "-- -- -- -- -- -- --")
      console.log(objProxy.name, "+ + + + + + +")})Copy the code
  • We don’t want to add reactiveFn to GET, thinking it’s a Dep action.

So we need to refactor the Depend class:

  • Solution to problem 1: Instead of using arrays, use sets.
  • Solution to problem two: Add a new method for collecting dependencies.
    // Save the current collection of reactive functions
    let activeReactiveFn = null

    /** * Depend optimization: * 1> Depend method * 2> Use Set to store dependent functions instead of arrays [] */

    class Depend {
      constructor() {
        this.reactiveFns = new Set()}// addDepend(reactiveFn) {
      // this.reactiveFns.add(reactiveFn)
      // }

      depend() {
        if (activeReactiveFn) {
          this.reactiveFns.add(activeReactiveFn)
        }
      }

      notify() {
        this.reactiveFns.forEach(fn= > {
          fn()
        })
      }
    }

    // Encapsulate a reactive function
    function watchFn(fn) {
      activeReactiveFn = fn
      fn()
      activeReactiveFn = null
    }

    // Encapsulate a function that gets depend
    const targetMap = new WeakMap(a)function getDepend(target, key) {
      // The process of getting a map based on the target object
      let map = targetMap.get(target)
      if(! map) { map =new Map()
        targetMap.set(target, map)
      }

      // Get the Depend object by key
      let depend = map.get(key)
      if(! depend) { depend =new Depend()
        map.set(key, depend)
      }
      return depend
    }

    function reactive(obj) {
      return new Proxy(obj, {
        get: function(target, key, receiver) {
          // Get the corresponding depend based on target.key
          const depend = getDepend(target, key)
          // Add a response function to depend
          // depend.addDepend(activeReactiveFn)
          depend.depend()

          return Reflect.get(target, key, receiver)
        },
        set: function(target, key, newValue, receiver) {
          Reflect.set(target, key, newValue, receiver)
          // depend.notify()
          const depend = getDepend(target, key)
          depend.notify()
        }
      })
    }
Copy the code

Reactive implementation in VUe2

    function reactive(obj) {
      // Before ES6, use Object.defineProperty
      Object.keys(obj).forEach(key= > {
        let value = obj[key]
        Object.defineProperty(obj, key, {
          get: function() {
            const depend = getDepend(obj, key)
            depend.depend()
            return value
          },
          set: function(newValue) {
            value = newValue
            const depend = getDepend(obj, key)
            depend.notify()
          }
        })
      })
      return obj
    }
Copy the code

test

    const objProxy = reactive({
      name: "zh"./ / object depend
      age: 22 / / object depend
    })

    const infoProxy = reactive({
      address: "hn".height: 1.80
    })

    watchFn(() = > {
      console.log(infoProxy.address)
    })

    infoProxy.address = "Beijing"

    const foo = reactive({
      name: "foo"
    })

    watchFn(() = > {
      console.log(foo.name)
    })

    foo.name = "bar"
    foo.name = "hhh"
Copy the code

To learn more about vue(2,3) and its ecology, visit my vue column.