vue change detection

Posted by luvburn on Thu, 27 Jan 2022 15:43:19 +0100

1. What is a watcher

The watcher is a mediator that notifies it when data changes, and then it notifies others.
For the watcher, see an example of using it:

//keypath
vm.$watch('a,b,c', function(newVal, oldVal){
	//do something
}
)

This code represents data a. When the B.C attribute changes, the function in the second parameter is triggered.
So how to realize this function? Just add the watcher instance to data a. In Dep of B.C attribute. Then, when data a. Notify the watcher when the value of B.C changes. Then, the watcher executes the callback function in the parameter.

export default class Watcher{
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        //Execute this Getter(), where you can read data a. Content of B.C
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();
    }
    get(){
        window.target = this;
        let value = this.getter.call(this.vm, this,vm);
        window.target = undefined;
        return value;
    }
    update(){
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

In the get method, first put window Set the target to this, that is, the current watcher instance, and then read the data a. The value of B.C, which triggers the getter.
Triggering getter will trigger the logic of collecting dependencies. As for collecting dependencies, you will see the Read a dependency from target and add it to Dep.
This leads to, as long as the window Target is attached with this, and then read the value again to trigger the getter to actively add this to the Dep of keypath.
After dependency is injected into Dep, whenever data a. When the value of B.C changes, all dependency loops in the dependency list will trigger the update method, that is, the update method in the watcher. The update method will execute the callback function in the parameter and pass value and oldValue to the parameter.
So, in fact, no matter the VM executed by the user$ Watch ('a.b.c ', (value, oldvalue) = > {}), or the data used in the template, all use the watcher to inform yourself whether changes need to be made.
How parsePath reads the keypath of a string is explained below:

const bailRE = /[^\w.$]/;
export function parsePath(path){
    if(bailRE.test(path)){
        return;
    }
    const segments = path.split(".")
    return function (obj){
        for(let i = 0; i < segments.length; i++){
            if(!obj){
                return;
            }
            obj = obj[segments[i]];
        }
        return obj;
    }
}

As you can see, first use keypath Divide into arrays, and then cycle the array layer by layer to read the data. Finally, the obj is the data you want to read in the keypath.

2. Recursively detect all key s

Now, in fact, the function of change detection can be realized, but the code described above can only detect a certain attribute in the data. We want to detect all attributes (including sub attributes) in the data, so we need to encapsulate an Observer class. The function of this class is to convert all attributes (including sub attributes) in a data into the form of getter/setter, and then track their changes:

export class Observer{
    constructor(value) {
        this.value = value;
        if (!Array.isArray(value)){
            this.walk(value)
        }
    }
    
    
    walk(obj){
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++){
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}


function defineReactive(data, key, val) {
    if (typeof val == 'object'){
        new Object(val);
    }
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.depend();
            return val;
        },
        set: function(newVal){
            if (val == newVal){
                return;
            }
            val = newVal;
            dep.notify();
        }
    })

}

In the above code, we define the Observer class, which is used to convert a normal object into a detected object.
Then judge the type of data. Only the data of object type will call walk to convert each attribute into the form of getter/setter to detect changes.
Finally, add new Observer(val) in definerevtive to recurse sub attributes, so that we can convert all attributes (including sub attributes) in data into getters / setters to detect changes.

3. Questions about Object

Previously, we introduced the change detection principle of Object type data, and learned that the change of data is tracked through getter/setter. Because of this tracking method, even if the data in some grammars changes, Vue I can't trace it.
For example, add an attribute to an Object:

var vm = new Vue({
    el: "#el",
    template: "#demo-tenplate",
    methods: {
        action(){
            this.obj.name = "berwin";
        }
    },
    data: {
        obj: {}
    }
})

In the action method, we added the name attribute on obj. vue cannot detect this change, so it will not send a notification to the dependency.
For example, delete an attribute from obj:

var vm = new Vue({
    el: "#el",
    template: "#demo-tenplate",
    methods: {
        action(){
            delete this.obj.name;
        }
    },
    data: {
        name: 'berwin'
    }
})

In the above code, we delete the name attribute in obj in the action method, but vue cannot detect this change, so it will not send a notification to the dependency.
vue through object Defineproperty is used to convert the key of the object into the form of getter/setter to track changes, but getter/setter can only track whether one data has been modified, and can not track new properties and deleted properties, which leads to the problems mentioned in the above example.

4. Summary

Change detection is to detect the change of data. When the data bar changes, it should be able to detect and send a notice.
Object can be accessed through object Defineproperty converts properties into getters / setters to track changes. getter will be triggered when reading data and setter will be triggered when modifying data.
We need to collect the dependency usage data in the getter. When the setter is triggered, we will notify the getter that the dependency data collected in the getter has changed.
To collect dependencies, we need to find a dependency place for dependencies. Therefore, we have created Dep, which is used to collect dependencies, delete dependencies and send messages to dependencies.
The so-called dependency is actually a timely watcher. Only the getter triggered by the watcher can collect dependencies. If the watcher triggers the getter, which Watcher will be collected in the Dep. When the data changes, it will cycle the dependency list and notify all watchers.

Topics: Javascript Front-end Vue.js