preface

How to detect array changes in Vue? If you are not familiar with this question, please learn together.
In the previous article, we mentioned the reactive principle of Vue2.0 and 3.0, but did not go into details. In this article, we will conduct an in-depth analysis of how Vue detects value changes of various data types in Vue2.0 and 3.0 respectively, so as to make the page responsive, and figure out why the array type changes should be treated specially. Finally, the reason why different data monitoring principles were adopted in the process of Vue upgrading from 2.x to 3.x was explored.


Start with a basic piece of code

The following code is very simple, students who have written Vue can understand what it is doing, but can you tell exactly how this code changes in the first second, second second and third second page respectively?


       
<html>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<body>
<div id="app">
  <div>{{ list }}</div>
</div>

<script>
new Vue({
  el: '#app'.data: {
    list: [],
  },
  mounted() {
	 setTimeout((a)= >{ 
     this.list[0] = 3
   }, 1000)
	 setTimeout((a)= >{
		 this.list.length = 5
	 }, 2000)
	 setTimeout((a)= >{ 
		 this.$set(this.list, this.list)
	 }, 3000)}})</script>
</body>
</html>Copy the code

You’d better start to copy the above code, local new HTML file save open debugging view, I directly here to say the results. When executing this code, the page does not change in the first and second seconds, and does not change until the third second. Consider that the first and second seconds change the value of the list. Why does Vue’s bidirectional binding fail here? Around this problem, let’s start with a step by step look at Vue’s data change monitoring implementation mechanism.

Vue2.0 data change monitor

Let’s start by looking at common data types.

1. The detection attribute is a basic data type

Listen to common data type, that is, to listen to the value of the object attribute is not the object of the five basic type changes, here do not directly look at the source code, each step is their own manual to achieve, more easy to understand.


       
<html>
  <div>
    name: <input id="name" />
  </div>
</html>
<script>
// Listen for the name attribute under Model, and cause the page id=name to change when the name attribute changes
const model = {
  name: 'vue'};// Create a listener with Object.defineProperty
function observe(obj) {
  let val = obj.name;
  Object.defineProperty(obj, 'name', {
    get() {
      return val;
    },
    set(newVal) {
      // When a new value is set, the setter is executed
      console.log('name changes: from${val}to${newVal}`);
      // Parse to the pagecompile(newVal); val = newVal; }})}// A parser that responds to the changed data on the page
function compile(val) {
  document.querySelector('#name').value = val;
}
// Call the listener and start listening to the Model
observe(model);
</script>Copy the code
Debug the process on the console.Copy the code

When debugging the above code, I first looked at the initial value of model.name, and then reset it to cause the setter function to trigger execution, so that the page can achieve a responsive effect.

But when you assign the name attribute to an object type, insert a key1 attribute into the new object, and then change the value of key1, the page does not fire in response.

Observe: Name is a normal datatype. Observe: name is a normal datatype. Observe: name is a datatype.

Let’s see how the observe function is implemented.

2. The detection attribute is the object type

From the above example, the test attribute values for an object, cannot meet the demand of listening, then further modification to observe surveillance function, the solution is simple, if the object is, once again, only with the current object, all the ordinary type of listening can change, if the object and object property, it is ok to continue to listen, if you know the recursive very well, Immediately you know how to solve the problem.


       
<html>
  <div>
    name: <input id="name" />
    val: <input id="val" />
    list: <input id="list" />
  </div>
</html>
<script>

// Listen for the name attribute under Model, and cause the page id=name to change when the name attribute changes
const model = {
  name: 'vue'.data: {
    val: 1
  },
  list: [1]};// Listen to the function
function observe(obj) {
  // Walk through all attributes, listening separately
  Object.keys(obj).map(key= > {
    // Make the object attribute special
    if (typeof obj[key] === 'object') {
      // is the object property listens again
      observe(obj[key]);
    } else {
      // Listen for non-object attributesdefineReactive(obj, key, obj[key]); }})}// Use Object.defineProperty to listen for Object attributes
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      return val;
    },
    set(newVal) {
      // When a new value is set, the setter is executed
      console.log(`${key}Change:${val}to${newVal}`);
      if (Array.isArray(obj)) {
        document.querySelector(`#list`).value = newVal;
      } else {
        document.querySelector(` #${key}`).value = newVal;
      }
      val = newVal;
      // The new property listens againobserve(newVal); }})}// Listen on all properties under model
observe(model);

</script>Copy the code
Debug the process on the console.Copy the code




In this case, I change the name property, fire the setter, and the page receives the response. I change the val property of the model.data object again, and the page gets the response. This shows that the previous problem that Observe could not monitor the change of the object’s property has been solved.

Now notice that at the end, I changed the first subscript of the list property to 5, and the page listened, but when I changed the second subscript, I didn’t fire the setter, and then I specifically changed the length of the list, or PUSH didn’t fire the setter of the array, and the page didn’t change the response.

Two questions are raised here:

A, I changed the value of the second subscript of array list, and the page does not respond to the change of array list when I call length and push.

B, back to the implementation of the Vue code at the beginning of the article, I changed the subscript attribute value of the list under Vue data, the page does not respond to change, but here I changed the value of the list from 1 to 5, the page responds, what is this?

Please continue with questions A and B.

3. Check that the property is an array object type

Here is the analysis of a problem to modify the array subscript value and call length, push methods to change the array does not trigger the setter function of the listener. I’ve read a lot of articles that Object. DefineProperty doesn’t listen for value changes in an array, is that true?

Take a look at the following example, where the page is not bound and the array elements that Object.defineProperty listens for can be heard for changes.

From the above code, first listen to all the attributes in the model array, and then through a variety of array methods to modify the current array, draw the following conclusions.

1. Modifying an existing element in an array can be listened on.

2. Arrays that operate on existing listening elements can also trigger setters to be listened on.

3. Only push, length, and pop special methods do not fire setters. This has to do with the internal implementation of methods and the implementation of setter hooks for Object.defineProperty.

4, When changing the value of a subscript that exceeds the array length, the value change is not heard. This is easy to understand, of course, does not exist to listen, because the binding listening operation has been performed before, the added element attribute at the time of binding, of course, there is no way to listen in advance.

Object. DefineProperty can’t listen for changes in array values. It is not supported to use Object.defineProperty to listen for non-existent array elements. And you can’t listen to any of the methods that make an array change.


4, explore Vue source code, see how to realize the array monitoring

For problem B, we need to look at the source code of Vue. Why Object. DefineProperty does not listen for array value changes when it can?

Here to share my view of the source code, if you open github line by line to see the source code is very difficult, I am here to directly use vuE-CLI to generate a local Vue project, and then install node_modules Vue package breakpoint view, you can try.

The test code is simple as follows;

import Vue from '. / node_modules/[email protected]@vue/dist/vue.runtime.com mon. Dev '
// instantiate Vue directly after startup
new Vue({
  data () {
    return {
      list: [1.3]}}})Copy the code

ArrayMethods is an array method that is overridden. The code flow is to modify push, pop, Shift, unshift, splice, sort, if there is a prototype. Reverse seven methods, if there is no prototype, go copyAugment to add the seven attributes after assigning the seven methods, and no listening.

/**
   * Observe a list of Array items.
   */
observeArray (items: Array<any>) {
  for (leti = 0, l = items.length; i < l; Observe (items[I])}}Copy the code

This. ObserveArray is a simple function that listens for elements in an array. Changing an element in an array cannot be listened on, but the value in the array is of the object type. Changing list[0] does not, but still does not listen for changes to the array itself.

Look again at how arrayMethods overrides the manipulation of arrays.

// Record API prototype methods before the original Array is overwritten
const arrayProto = Array.prototype
// Make a copy of the above prototype
const arrayMethods = Object.create(arrayProto)
// The method to override
const methodsToPatch = [ 'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse' ]

/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (. args) {
    // The original array method call is executed
    const result = arrayProto[method].apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // If it is inserted, listen again
    if (inserted) ob.observeArray(inserted)
    // Trigger the subscription, such as the page update response is triggered here
    ob.dep.notify()
    return result
  })
})Copy the code

From the above source code can be seen in Vue2. X to rewrite the array method idea, rewrite the array will be in each array after the implementation of the original method manually triggered response page effect.

After reading the source code, problem A also came to light, Vue2. X did not implement the existing array elements to do monitoring, but to monitor the array changes caused by the method, trigger this method at the same time to call the mounted response page method, to achieve the page response type effect.

Note that not all array methods have been rewritten, just push, POP, Shift, unshift, splice, sort, and reverse. Why not use Object.defineProperty to listen for changes to existing elements in an array?

The author’s concern is that for performance reasons, listening on each array element binding is costly and not beneficial.

Issue address: https://github.com/vuejs/vue/issues/8562.

Vue3.0 data change monitor

As mentioned in the previous article, Vue3.0 uses the new ES6 construction method Proxy to detect changes in the original object. (For those unfamiliar with Proxy, you can read the previous article.) In this layer, you can listen for changes in arrays and various data types, as shown in the following example.

Simply perfect, whether the array subindex assignment caused by changes or changes in the array method, can be monitored, and can not only avoid listening to the performance of each property of the array caused by the problem, but also can solve the problem like POP, push method, length method to change the array can not be monitored when the array changes.

Next, use Proxy and Reflect to implement bidirectional binding under Vue3.0.

<! DOCTYPE html> <html> <div> name: <input id="name" /> val: <input id="val" /> list: <input id="list" /> </div> </html> <script> let model = { name: 'vue', data: { val: 1, }, list: [1] } function isObj (obj) { return typeof obj === 'object'; Function observe(data) {object.keys (data).map(key => {if (isObj(data[key])) {if (isObj(data[key])) data[key] = observe(data[key]); } }) return defineProxy(data); Function defineProxy(obj) {return new Proxy(obj, {set(obj, key), Val) {console.log(' property ${key} changes to ${val} '); compile(obj, key, val); return Reflect.set(... arguments); }})} // Parser, Function compile(obj, id, Var) {if (array.isarray (obj)) {document.querySelector('#list').value = model.list; } else { document.querySelector(`#${id}`).value = val; } } model= observe(model); </script>Copy the code

With Proxy and Reflect, you don’t have to worry about whether an array operation triggers a setter or not. As long as the operation passes through the Proxy layer, the various operations will be captured and the page responsive requirements will be met.

conclusion

The problem of array change monitoring in Vue2. X is not that Object. DefinePropertype method can’t detect array changes. That’s the rewrite of the array mentioned above.

The way of using Proxy in Vue3.0 perfectly solves the problems in 2.0, so if you encounter the processing of array listening in Vue in the future interview, you must distinguish which version is clear, the end of this article.


The above content is their own summary, it is inevitable that there will be mistakes or misunderstanding, if there are questions, I hope you leave a message to correct, so as not to mistake people, if there are any questions please add group questions, also welcome to add the author’s wechat WX: GTR_9360, will try to answer it. If it’s helpful, don’t forget to share it with your friends or click “Watching” in the lower right corner. You can also follow the author, view historical articles and follow the latest developments to help you become a full stack engineer!