I have previously written a responsive principle – how to listen for Array changes, and recently I am going to share it with my team colleagues, but I find it is too rough to read before, so I decide to write another detailed version ~

How to listen for array index changes?

(1) Case analysis

Change the index of an array without triggering an update to the view. Take this example:

var vm = new Vue({
  data: {
    items: ['a'.'b'.'c']
  }
})
vm.items[1] = 'x' // Not responsive
Copy the code

The above case copy Vue official document – array update detection.

(2) Solution

Use vue.set to trigger view updates.

// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
Copy the code

(3) Why can’t Vue listen for index changes?

Vue officials gave an explanation that it could not be tested.

Due to JavaScript limitations, Vue cannot detect changes to an array when you set an array item directly using the index, such as VVM. Items [indexOfItem] = newValue.

So what’s the reason? In the process of learning, I found that many articles were taken out of context. Vue official explained that “Vue cannot detect”, while many articles wrote that “object.defineProperty cannot detect”.

But in fact, object.defineProperty can detect changes to the array index. Here’s a case:

let data = [1.2];
function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: (a)= > {
            console.log('I've been read, should I do something? ');
            return val;
        },
        set: newVal= > {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log("The data has been changed, I need to render to the page!");
        }
    })
}

defineReactive(data, 0.1);
console.log(data[0]);
data[0] = 5;
Copy the code

You can try it yourself on the console, and the answer is pretty obvious.

Vue just doesn’t listen for array index changes in this way, because You think the performance cost is too high, so you make a trade-off between performance and user experience. See why Vue cannot detect array changes.

Vue can’t detect array changes because it doesn’t do that.

But we developers certainly have this need, and the solution is as follows, using vue.set.

// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
Copy the code

The principle? Obviously, there is no loop listening for all array indexes in the initial process, but the developer needs to know which index to listen for. Vue.set will help you listen to which one, core or object.defineProperty. Just as much as possible to avoid useless array index listening.

How to listen for the increment or decrease of the contents of an array?

(1) Skill limitation

Object.defineproperty can detect index changes, but it does not listen for array additions or deletions. For more information, read the Vue official document – Precautions for Object Change Detection.

What does Vue do at this point?

(2) Clever solutions

Vue’s solution is to rewrite the array prototype, more accurately to intercept the array prototype.

First, I selected seven methods that can change the array itself. Next, let’s look at the case:

// Get the method on the prototype
const arrayProto = Array.prototype;

// Create a new object, using the existing object to provide the __proto__ of the newly created object
const arrayMethods = Object.create(arrayProto); 

// Do some interception operations
Object.defineProperty(arrayMethods, 'push', { value(... args) {console.log('User-passed parameters', args);

        // True push ensures that the data is as expected
        arrayProto.push.apply(this, args);
    },
    enumerable: true.writable: true.configurable: true});let list = [1];

list.__proto__ = arrayMethods; // Reset the prototype

list.push(2.3);

console.log('User gets list:', list);

Copy the code

Why is it called interception? When we override the push method in our case, we also need to use a real push to ensure that the array is pushed as the user expects.

As you can see, we can listen for the parameters that the user passed in, so we can listen for the array to change, and we can make sure that the array is pushed as the user expects.

The reason we use arrayMethods to inherit from the real prototype is that we don’t pollute the global array. prototype, because the Array we’re listening on is only in vm.data.

(3) Source code analysis

export class Observer {
    constructor (value: any) {
        // If it is an array
        if (Array.isArray(value)) {
            // If the prototype has a __proto__ attribute, it is mainly for the browser to determine compatibility
            if (hasProto) {
                // Override the prototype of the responsive object directly
                protoAugment(value, arrayMethods)
            } else {
                // Copy directly to the properties of the object, because when you access an object's method, you first look for its own, and then look for its prototype
                copyAugment(value, arrayMethods, arrayKeys)
            }
        } else {
          // If it is an object
          this.walk(value); }}}Copy the code

You can see the special handling of arrays by the Observer above.

(4) How does the array collect dependencies and distribute updates?

We know that objects collect dependencies in getters and dispatch updates in setters. Just remember:

function defineReactive (obj, key, val) {
    // Generate an instance of Dep
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get: (a)= > {
            // Dependency collection
            dep.depend();
        },
        set: (a)= > {
            // Distribute updatesdep.notify(); }})}Copy the code

To ensure that each data in the data has a one-to-one DEP, closures are applied to ensure that each instance of the DEP is not destroyed. The problem is that dep is a local variable, and listening for array changes requires an update in the array interceptor. Then you won’t be able to access the DEP, and you won’t know which Watcher to notify!

So what does Vue do? Since this is not accessible, then another DEP.

export class Observer {
    constructor (value: any) {
        this.value = value / / data attributes
        this.dep = new Dep() // Mount the DEP instance
        // Define a __ob__ attribute for the data, the value of which is the current Observer instance object
        def(value, '__ob__'.this) // Attach the current Observer instance to data's __ob__}}Copy the code

During Vue initialization, the current Observer instance is mounted for each data in the data, and deP is mounted on that instance. This ensures that we can access the DEP in the array interceptor. As follows:

Object.defineProperty(arrayMethods, 'push', { value(... args) { console.log('User-passed parameters', args); Arrayproto.push. apply(this, args); arrayProto.push.apply(this, args); __ob__ console.log(this.__ob__.dep)}, enumerable: __ob__ console.log(this.__ob__.dep)}true,
    writable: true,
    configurable: true});Copy the code

Now we can execute dep.notify() in the interceptor.

So how do you collect dependencies?

// Obtain the observe instance on the current data, that is __ob__letchildOb = ! shallow && observe(val);functionDefineReactive (obj, key, val) {// create a Dep instancelet dep = new Dep();
    Object.defineProperty(obj, key, {
        get: () => {
            if(dep.target) {// rely on collection dep.depend(); // Collect the second timeif(childob.dep) {// collect dependencies again childob.dep.depend (); }}returnval; }})}Copy the code

Now we’re going to store two dePs, which of course we’re going to collect twice in the getter, and childOb is actually the __ob__ returned by the observe. Don’t care about the details, to see the source code to know ~

(5) Summary

To summarize, collect dependencies in getters for arrays and trigger updates in interceptors.

Third, other thinking

(1) Think: where else can __ob__ be used?

  1. Determines whether an array has been obsered to avoid repeated execution.

  2. Vue.set and vue. del are required to access deP.

(2) Does array assignment change length?

Because object.defineProperty cannot detect array length changes, for example: vm.items.length = newLength.

Var = new vm Vue ({data: {items: [' a ']}}) / / assignment again, change the length of the vm. The items = [' a, 'b', 'c']Copy the code

Vue = [‘a, ‘b’, ‘c’]; Vue = [‘a, ‘b’, ‘c’]; In this case, we are actually listening on the items property of the OBJECT VM, and arrays are actually irrelevant. Because it was found that some people misunderstood, here is a simple hint ~

Four,

This article is mainly about principles and ideas, and will not involve a lot of code, after all, the source code will always change. Make sure your JS foundation is solid even at the same time, read source code just won’t laboriously oh ~ I am very laboriously the kind of 😭

If you think it helps you, just give it a like

Has been completed:

Vue source code interpretation series

  • 1. Vue responsive Principles – Understand Observer, Dep, Watcher
  • 2. Reactive Principle – How to listen for Array changes
  • 3. Reactive Principle – How to listen for Array changes? A detailed version
  • 4. Vue asynchronous update – nextTick Why microtask first?

Welcome to the Github blog

5. References

  • Remember a question to think about: Why can’t Vue detect array changes
  • Book “Vue.js”