Data response

What is the data response?

DOM view changes are driven by data changes.

What is the difference between Vue2 and Vue3 in implementing data responsiveness?

Vue2 responsiveness: Implemented through Object.defineProperty. Hijacks listeners for each property of a data object.

let data = {
  name: 'bill'.car: 'land rover'.work: {
    a: '1'
  }
}

observer(data);

function observer(data) {
  if (typeofdata ! = ='object' || data == null) {
    return;
  }
  // In the case of objects, reactive functions need to be called recursively
  const keys = Object.keys(data);

  for (let i = 0; i < keys.length; i++) {
    let key = keys[i];
    letvalue = obj[key]; reactive(obj, key, value); }}// 'object.defineProperty ()' is the core of Vue2
// When Vue2 is initialized, it hijacks data recursively if the attributes are still objects.
// response function
function reactive(obj, key, value) {
  observer(value);

  Object.defineProperty(obj, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue === value) return; observer(newValue); value = newValue; }})}Copy the code

💥 What are the disadvantages of implementing responsiveness through Object.defineProperty?

  1. We can see from the code aboveObject.defineProperty()Is flawed and can be costly in performance when the data being observed is deeply nested.
  2. There is only hijacking of properties in the original object, when the objectAdd a propertyorRemove an attribute, are non-responsive. Therefore, byVue.set()orVue.delete()Solve such problems.
  3. Can’tListen for direct assignment of array indexes,Can’tListen to modify the length of the array, so Vue2 intercepts calls by modifying the inheritance relationship of the array and rewriting the array method. Like these array methodspush.pop.shift.unshift.splice.sort.reverse.

In Vue2, a page update is not triggered by a direct assignment from an array index value, arr[0]=’newVal’. It’s not that Object.defineProperty can’t do this, it’s that it’s not done because of performance concerns. Because the length of the array is uncertain, hijacking array traversal can cost a lot of performance.

Vue3 responsive: Use the new Proxy in ES6. Let’s take a look at a Proxy example.

// Define the handler function
const handler = {
  // get catcher - gets the property value
  get (target, prop) {
    console.log('Intercepts read data: property${prop}`)
    return target
  },
  // Set catcher - modify attribute values or add attributes
  set (target, prop, value) {
    target[prop] = value
    console.log('Intercepts modifying data or adding attributes: attributes${prop}, the attribute values${value}`)
    return target
  },
  // deleteProperty catcher - Deletes an attribute
  deleteProperty (target, prop) {
    delete target[prop]
    console.log('Intercepted delete data: property${prop}`)
    return target
  }
  / /... Total of 13 configuration items (capture)
}

// use Proxy, p is the object after Proxy
const p = new Proxy({}, handler);

// Validate modifying/adding attributes (which goes to handler's set catcher method)
p.name = 'li mei'
// Validate read properties (go to handler's set catcher method)
p.name
// Verify the deletion of the property (which goes to the handler's deleteProperty catcher method)
delete p.name
Copy the code

💥 From the above examples, we can see the advantages of Vue3 using Proxy to achieve responsiveness:

  1. Can be hijackedThe entire object(instead of just hijacking properties), and return a new object.ProxyIn terms of the amount of codeObject.defineProperty()Data hijacking operation of.
  2. ProxyProvides 13 kinds of hijacking capture operation, can be more refined hijacking capture operation, which isObject.definePropertyIt can’t be done.

Compatibility between Vue2 and Vue3

Because Object. DefineProperty is compatible with IE8, Vue2 is generally compatible with IE8. Proxy is incompatible with IE11, so Vue3 is currently incompatible with IE11.

Explain Proxy and Reflect

The Proxy defined?

Now that you know that Vue3 implements data responsiveness through Proxy, let’s look at how it is used.

One of the most obvious metaprogramming features added to ES6 is the Proxy feature.

In simple terms, a proxy can be seen as a “wrapper” around a target object, placing a layer of interception on the target object through which all external access to the object must pass. Provides 13 kinds of catchers like GET,set,deleteProperty and so on.

The Proxy used?

Grammar:

const p = new Proxy(target, handler)
Copy the code

Parameters:

  • target

  • The target object (which can be any type of object, including a native array, function, or even another Proxy) that you want to wrap with a Proxy.

  • handler

  • Proxy configuration: Objects with “traps” (methods of intercepting operations). For example, the get catcher method reads target’s properties, the set catcher method writes target’s properties, and so on.

let target = {}; 
let proxy = new Proxy(target, {}); // Empty handler object

proxy.test = 5; // Write proxy object (1)
console.log(target.test); // 5, the test attribute appears in target!
console.log(proxy.test); // 5, we can also read it from the proxy object (2)

for(let key in proxy) console.log(key);// test, iteration also works (3)
Copy the code

In the above example 🌰, all operations on the proxy are forwarded directly to Target because there is no catcher (empty handler object).

  1. Write operationproxy.testWill write the valuetarget.
  2. Read operationproxy.testfromtargetReturns the corresponding value.
  3. The iterationproxyfromtargetReturns the corresponding value.

We can see that the proxy is a transparent wrapper for target without any catchers.

Of course, you can add the capture (13) to intercept, expand the activation of proxy more functions.

First, for most operations on objects, there is a so-called “inner method” in the JavaScript specification that describes how the lowest level of operations work. For example, [[Get]], internal methods for reading properties, [[Set]], internal methods for writing properties, and so on. These methods are only used in the specification and we cannot call them directly by method name.

The Proxy capture intercepts the invocation of these methods. For each internal method, there is a catcher in the table: the catcher can be added to the handler parameter of the new Proxy to intercept the use of the object’s internal method:

Click on MDN to see how/when these 13 traps trigger.

💥 Why use Proxy?

  1. The proxy becomes the primary object of code interaction, while the actual target object remains hidden/protected.
  2. You can intercept (and override) almost all of an object’s behavior, which means you can extend object features beyond JavaScript content in powerful ways.
  3. Reduce the complexity of functions or classes

Reflect the definition of

Reflect is called reflection. It is also a new API for manipulating objects in ES6, instead of calling objects directly.

Reflect is a built-in object that provides methods to intercept JavaScript operations.

The internal methods of objects, such as [[Get]] and [[Set]], are normative and cannot be called directly.

The Reflect object makes it possible to call these internal methods. Its methods are minimal wrappers of internal methods.

Here is an example of the same action and Reflect call:

For example, we can replace obj[prop] = value with reflect. set:

let user = {};
Reflect.set(user, 'name'.'Fengmaybe');
console.log(user.name); // Fengmaybe
Copy the code

So, it’s easier to use Reflect to forward the action to the original object. That’s one of the reasons Reflect came along: manipulating objects is easier and more semantic.

You can go to MDN to see a comparison between Object and Reflect

Reflect is also typically used on forward objects in proxies. For each internal method that can be captured by Proxy, there is a corresponding method in Reflect with the same name and parameters as the Proxy capture. This is crucial, as the 13 catchers correspond to the Reflect method one by one, simplifying Proxy creation. That’s one of the reasons Reflect came along: it works perfectly with Proxy. Look at the following example 🌰 :

let p = new Proxy(target, {
  get (target, prop, receiver) {
    // Proxy's get capture corresponds to the reflect. get method name and parameter
    return Reflect.get(target, prop, receiver);
    // Reflect. Get (... arguments)}});Copy the code

💥 Summarizes some of Reflect’s features:

  1. Unconstructible and cannot be called with new
  2. All methods andProxyHandlers are the same (13 one to one)
  3. Provides more API thanObjectRicher and more semantic to use
  4. All methods are static methods, likeMath. Static methods can be used directlyClass name. Method nameTo call; Instance methods are not allowed. You must use an instance to call them.
  5. Partial method andObject.*Same, but with slightly different behavior. Such asObject.defineProperty(obj, name, desc)An error is thrown if the attribute cannot be defined, andReflect.defineProperty(obj, name, desc)Will returnfalse. (See 🌰 for example)
//Object Object method
try {
  Object.defineProperty(target, name, property);
} catch (e) {
  console.log("error");
}

//Reflect object method
if (Reflect(target, name, property)) {
  console.log("success");
} else {
  console.log("error")}Copy the code

The receiver parameter is irreplaceable

Let’s look at an example 🌰 to illustrate why reflect.get is better. Also, we’ll see why get/set has a third parameter, receiver, and how to use it.

let animal = {
  _name: "Animal".get name() {
      return this._name; }};let animalProxy = new Proxy(animal, {
  get(target, prop) {
      return target[prop]; // (*) target = cat}});let cat = {
  __proto__: animalProxy,
  _name: "Cat"
};

// Expected output: cat
console.log(cat.name); // Output: animal
Copy the code

Reading cat.name should return “cat”, not “animal”!

What happened? Maybe we did something wrong with inheritance?

However, if we remove the agent, then everything goes as expected.

The problem is actually in the agent, in the (*) line.

  1. When we read cat.name, since the cat object itself has no corresponding attribute, the search will go to its prototype.

  2. The prototype is animalProxy.

  3. When the name property is read from the proxy, the GET catcher is fired and returns the Target [prop] property from the original object, on the (*) line.

When target[prop] is called, if prop is a getter, it runs its code in the context of this=target. Thus, the result is this._name from the original target object, which is from Animal.

To resolve this situation, we need the third parameter of the GET catcher, receiver. It ensures that the correct this is passed to the getter. In our case, cat.

How do I pass the context to the getter? For a regular function, we can use call/apply, but this is a getter that cannot be “called”, only accessed.

Reflect.get can do that. If we use it, everything will work fine.

This is the corrected variant:

let animal = {
  _name: "Animal".get name() {
      return this._name; }};let animalProxy = new Proxy(animal, {
  get(target, prop, receiver) {
      return Reflect.get(target, prop, receiver); / / (*)}});let cat = {
  __proto__: animalProxy,
  _name: Maine Coon
};

// Expected output: Maine coon
console.log(cat.name); // Output: cat
Copy the code

The receiver now retains a reference to the correct this (i.e. Cat) that is passed to the getter on the (*) line via reflect.get.

You can see that the third parameter of the catcher, receiver, is irreplaceable.

We can rewrite the catcher to be shorter:

get(target, prop, receiver) { 
  return Reflect.get (...arguments); 
}
Copy the code

The Reflect call is named exactly the same as the catcher and takes the same arguments. They are specially designed in this way.

So return Reflect… Provides a secure way to easily forward operations and ensure that we don’t forget anything about it.

ProxyandReflectIt’s always collaborative

As you can see from the last example in the previous section, 🌰, Proxy and Reflect were designed to work perfectly together from the start.

💥 now summarize the reasons why the two work together:

  1. ReflectThe Api has 13 static functions, which is the same asProxyThe design is one to one. ifProxyA catcher that wants to forward a call to an object simply calls it with the same argumentsReflect.<method>That’s enough. This mapping is intended to be symmetrical from the beginning of design.
  2. The Proxy get/set() method returns the same value as Reflect’s get/set method and can be used naturally in conjunction with it, making it more convenient and accurate than assigning/getting a value directly from an object.
  3. The receiver parameter is irreplaceable.

ReflectandProxyUse in combination to achieve responsiveness

Now we will use Proxy + Reflect to transform and achieve data responsive effect.

// Define the handler function
const handler = {
  // Get the attribute value
  get (target, prop) {
    console.log('Intercepted read data')
    // ***等同于target[prop]
    const result = Reflect.get(target, prop)
    return result
  },
  // Modify attribute values or add attributes
  set (target, prop, value) {
    console.log('Intercepts modifying data or adding attributes: attributes${prop}, the attribute values${value}`)
    // ***等同于target[prop] = value
    const result = Reflect.set(target, prop, value)
    return result
  },
  // Delete an attribute
  deleteProperty (target, prop) {
    console.log('Intercepted delete data: property${prop}`)
    // ***等同于delete target[prop]
    const result = Reflect.deleteProperty(target, prop)
    return result
  }
  / /... There are 13 configuration items
}
const p = new Proxy({}, handler);
// Verify to modify or add attributes
p.b = undefined;
// Validate to get the property value
p.b
// Verify that an attribute is deleted
delete p.b
Copy the code

As you can see from the example above, the same effect could be achieved if we didn’t use Reflect. However, we usually use the Proxy + Reflect API with the following considerations:

  1. As long as it isProxyObject has a proxy method,ReflectObjects all correspond to,. Therefore, no matterProxyHow can I change the default behavior to always passReflectThe corresponding method gets the default behavior.
  2. useReflectAPI modification behavior does not report errors and is more reasonable to use. For example, as mentioned aboveObject.defineProperty(obj, name, desc)andReflect.defineProperty(obj, name, desc).
  3. ReflectProviding this static method call is more semantic.

Vue3 composite API implementation principle

preface

Vue3 introduces a new feature, the composite API. Some of these responsive apis are implemented through Proxy+Reflect. List some of the responsive apis to see how they are implemented. The specific functions and functions of THE API can be seen on the official website. It is not described here, but only for its internal implementation.

The principle of

How responsive apis are implemented

  • Proxy: Intercepts the change of any attribute in the object, including reading and writing of attribute values, adding and deleting attributes.
  • Through Reflect: To operate on the properties of the proxied object.

implementation

We are only implementing simple logical orchestration of data in a responsive manner, and we are not implementing DOM variations.

1. shallowReactiveandreactive

So let’s start with shallowReactive and reactive.

Two points need to be made:

  1. callshallowReactive(obj)Will return a proxied object, soshallowReactiveThe function should return oneproxy:new Proxy(target, reactiveHandler).
  2. callreactive(obj)Will return a deeper proxy object, so inshallowReactiveThe function is implemented recursively to determine whether its value is an object type, and if so, it needs torecursiveperformnew Proxy(target, reactiveHandler)This operation. Having each layer of the object return a responsive proxy object naturally returns a deeper proxy object.
// shallowReactive(shallow hijacking, shallow monitoring, shallow response data) and Reactive(deep)

// Define a reactiveHandler handler object
const reactiveHandler = {
  // Get the attribute value
  get (target, prop) {
    const result = Reflect.get(target, prop)
    console.log('Intercepted read data', prop, result)
    return result
  },
  // Modify attribute values or add attributes
  set (target, prop, value) {
    const result = Reflect.set(target, prop, value)
    console.log('Intercepted modified data or added attributes', prop, value)
    return result
  },
  // Delete an attribute
  deleteProperty (target, prop) {
    const result = Reflect.deleteProperty(target, prop)
    console.log('Deletion data intercepted', prop)
    return result
  }
}

// Define a shallowReactive function that passes in a target object
function shallowReactive (target) {
  // Check whether the current target object is of type object (object/array)
  if (target && typeof target === 'object') {
    return new Proxy(target, reactiveHandler)
  }
  // If the data passed in is of a primitive type, it is returned directly
  return target
}

// Define a reactive function that passes in a target object
function reactive (target) {
  // Check whether the current target object is of type object (object/array)
  if (target && typeof target === 'object') {
    // Perform recursive processing of all data in arrays or objects reactive
    // Check whether the current data is an array
    if (Array.isArray(target)) {
      // The array is traversed
      target.forEach((item, index) = > {
        target[index] = reactive(item)
      })
    } else {
      // Determine if the current data is an object
      // The data of the object is also traversed
      Object.keys(target).forEach(key= > {
        target[key] = reactive(target[key])
      })

    }
    return new Proxy(target, reactiveHandler)
  }
  // If the data passed in is of a primitive type, it is returned directly
  return target
}
Copy the code

This makes it easy for shallowReactive and reactive functions to respond to read/write/delete operations. The reactiveHandler handler function can be extracted separately for shallowReactive and reactive functions.

✅ Now use the shallowReactive and reactive functions written by yourself to see if they work as expected.

    // Verify that shallowReactive is used
    const proxyUser1 = shallowReactive({
      name: 'Ming'.car: {
        color: 'red'}})// Intercepts read and write data
    proxyUser1.name +='= ='
    Read data is intercepted, but write data is not intercepted
    proxyUser1.car.color+'= ='
    // Delete data was intercepted
    delete proxyUser1.name
    // Only read is blocked, but delete is not blocked
    delete proxyUser1.car.color
 
    // Verify using reactive
    const proxyUser2 = reactive({
      name: 'Ming'.car: {
        color: 'red'}})// Intercepted read and modified data
    proxyUser2.name += '= ='
    // Intercepted read and modified data
    proxyUser2.car.color = '= ='
    // The deletion was blocked
    delete proxyUser2.name
    // Read intercepts and delete intercepts
    delete proxyUser2.car.color
Copy the code

You can also check shallowReactive and reactive functions by clicking on the Codepen workbench

2. shallowReadonlyandreadonly

Let’s take a look at shallowReadonly and the readOnly implementation

The shallowReadonly and readOnly functions are almost identical to shallowReactive and reactive functions.

The only difference is in the handler. Readonly means readonly, so reflect. get(target, prop) is only executed in get. It does not manipulate the object’s behavior elsewhere, only returns a Boolean. This enables properties in the proxy object to be read only and cannot be modified/added/deleted.

// Defines a readonlyHandler handler
const readonlyHandler = {
  get (target, prop) {
    const result = Reflect.get(target, prop)
    console.log('Intercepted read data', prop, result)
    return result
  },
  set (target, prop, value) {
    console.warn('Can only read data, cannot modify data or add data')
    return true
  },
  deleteProperty (target, prop) {
    console.warn('Only read data, not delete data')
    return true}}// Define a shallowReadonly function
function shallowReadonly (target) {
  // We need to determine whether the current data is an object
  if (target && typeof target === 'object') {
    return new Proxy(target, readonlyHandler)
  }

  return target
}
// Define a readonly function
function readonly (target) {
  // We need to determine whether the current data is an object
  if (target && typeof target === 'object') {
    // Check whether target is an array
    if (Array.isArray(target)) {
      // go through the number group
      target.forEach((item, index) = > {
        target[index] = readonly(item)
      })
    } else { // Determine whether target is an object
      // Iterate over the object
      Object.keys(target).forEach(key= > {
        target[key] = readonly(target[key])
      })
    }
    return new Proxy(target, readonlyHandler)
  }
  // If it is not an object or array, return it directly
  return target
}

Copy the code

✅ verify that shallowReadonly and the readonly function have the desired effect.

    // Test shallowReadonly and readonly function implementations
    const proxyUser3 = shallowReadonly({
      name: 'Ming'.cars: ['Mercedes'.'BMW']})// It can be read
    console.log(proxyUser3.name)
    // Cannot be modified
    proxyUser3.name = '= ='
    // Cannot be deleted
    delete proxyUser3.name
    // Intercepts the read, can be modified (because it is shallow response)
    proxyUser3.cars[0] ='audi'
    // The read is intercepted and can be deleted
    delete proxyUser3.cars[0]

    const proxyUser4 = readonly({
      name: 'Ming'.cars: ['Mercedes'.'BMW']})// The read was intercepted
    console.log(proxyUser4.name)
    console.log(proxyUser4.cars[0])
    / / read-only
    proxyUser4.name='ha ha'
    // Read-only (because it is deep responsive)
    proxyUser4.cars[0] ='ha ha'
    delete proxyUser4.name
    delete proxyUser4.cars[0]
Copy the code

You can also click on the Codepen workbench to verify the shallowReadonly and readOnly function implementations

3. shallowRefandref

Take a look at the shallowRef and REF implementations

ShallowRef and ref differ from the above examples in that they return a reactive proxy object with only a.value property that points to the target object to which the value is assigned.

const count = ref({name:'Ming'})
// Only.value can be accessed
console.log(count.value) // {name:' xiaoming '}
Copy the code

So the shallowRef and ref functions are designed to return an object with accessor properties of GET/SET that go to get when reading properties and set when modifying properties.

// Define a shallowRef function
function shallowRef (target) {
  return {
    // Save the target data
    _value: target,
    get value () {
      console.log('Hijacked to read data')
      return this._value
    },
    set value (val) {
      console.log('Hijacked modify data, ready to update interface', val)
      this._value = val
    }

  }
}

// define a ref function
function ref (target) {
  // If an object is assigned a ref value, it is treated as a deep responsive object. Call reactive
  target = reactive(target)
  return {
    // Save the target data
    _value: target,
    get value () {
      console.log('Hijacked to read data')
      return this._value
    },
    set value (val) {
      console.log('Hijacked modify data, ready to update interface', val)
      this._value = val
    }
  }
}
Copy the code

✅ verify the implementation of shallowRef and ref response expressions.

/ / verification
const ref1 = shallowRef({
  name: "Xiao Ming".car: {
    color: "red"}});console.log(ref1.value);
// Hijack to (can hijack to listen to the change operation)
ref1.value = "= =";
// Hijacking failed to modify attributes (car attributes are not hijacked when modified)
ref1.value.car = "= =";

const ref2 = ref({
  name: "Xiao Ming".car: {
    color: "red"}});console.log(ref2.value);
// Hijacking to (modify the value of ref2 can be hijacked to listen)
ref2.value = "= =";
// Hijacked to read data (modify ref2 deep attributes can also be hijacked to listen)
ref2.value.car = "= =";
Copy the code

You can also verify the shallowRef and ref function implementations by clicking on the Codepen workbench

4. isRef/isReactive/isReadonly/isProxy

See how isRef/isReactive/isReadonly/isProxy function design

These functions return a Boolean value. You just need to design a property that hangs in the handler that generated the proxy object.

// define a function isRef to determine whether the current object is a ref object
// The new _is_ref attribute will be mounted to the return object of the ref function
function isRef (obj) {
  return obj && obj._is_ref
}

// Define a function isReactive to check whether the current object is a reactive object
// Add the _is_reactive property to be mounted to the handler in reactive
function isReactive (obj) {
  return obj && obj._is_reactive
}

// Define a function isReadonly to determine whether the current object is a readOnly object
// Add a handler to mount the _is_readonly attribute to the readonly function
function isReadonly (obj) {
  return obj && obj._is_readonly
}

// Define a function isProxy to check whether the current object is reactive or readonly
function isProxy (obj) {
  return isReactive(obj) || isReadonly(obj)
}
Copy the code

📍 added the _is_ref attribute to mount to the return object of the ref function

function ref (target) {
  target = reactive(target)
  return {
    _is_ref: true.// Identifies the current object as the ref object !!!!
    _value: target,
    get value () {
      return this._value
    }
    // ...}}Copy the code

📍 Add the _is_reactive property to mount to the handler in the reactive function

// Define a reactiveHandler handler object
const reactiveHandler = {
  get (target, prop) {
    // if accessed through obj._is_reactive, this is where it comes in and returns true
    if (prop === '_is_reactive') return true
    const result = Reflect.get(target, prop)
    return result
  }
  // ...
}
Copy the code

📍 added a handler to mount the _is_readonly attribute to the readonly function

// Defines a readonlyHandler handler
const readonlyHandler = {
  get (target, prop) {
    // If accessed through obj._is_readonly, it will come here and return true
    if (prop === '_is_readonly') return true
    const result = Reflect.get(target, prop)
    return result
  },
  // ...
}
Copy the code

✅ Simple verification

    // All of the following return true
    console.log(isRef(ref({})))
    console.log(isReactive(reactive({})))
    console.log(isReadonly(readonly({})))
    console.log(isProxy(reactive({})))
    console.log(isProxy(readonly({})))
Copy the code

You can also click to codepen isRef under the workbench to test/isReactive/isReadonly/isProxy function implementation

Composition API VS Option API

In the last section, we looked at the Composition API of Vue3 through Proxy. So let’s take a look at why we chose to discard Vue2’s Option API.

Problem with Option API

In the traditional Vue Options API, to add or modify a requirement, you need to move the scroll bar up and down repeatedly in Data, Methods, and computed.

Use the Compisition API

We can organize our code and functions more elegantly. Keep the code of related functions organized.

When we need to add a feature to the original page, the Compisition API allows us to keep the code for the added feature as fragmented as possible and place it in an orderly manner, thus better realizing the concept of “function code block” for easy maintenance.