Vue: Object change detection

Vue: Object change detection

1. What is change detection

​ Vue.js will automatically detect the state and generate the DOM, and then output it to the page. This process is called rendering. This rendering process is declarative. We use templates to describe the mapping relationship between the state and the DOM.

​ Under normal circumstances, our page is constantly updating the state. At this time, the page is re-rendered according to the state. If you want to detect this process, it involves change detection .

​ There are two types of change detection: "push" and "pull". Angular and React in the three major domestic frameworks both use "pull", while Vue uses "push" .

​ The advantage of "push" is that when the state changes, Vue can immediately know and push the information to all related dependencies. Therefore, the more information is known, the more fine-grained updates can be made.

​ The finer the granularity, the greater the memory overhead consumed by tracking dependencies. Vue introduces the virtual DOM and adjusts the granularity to a medium granularity. The dependency bound to a state is no longer a specific DOM node, but a component. The component then notifies the DOM, which greatly reduces the number of dependencies and reduces memory consumption.

2. Tracking changes and collecting the implementation of dependencies

2.1 Object tracking changes in JS

​ In JS, there are two main methods of detecting object changes: Object.definePropertyand Proxy.

​ Let's Object.definePropertyencapsulate it to make it responsive:

function defineReactive(data,key,val){
    
    
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get: function(){
    
    
            return val;
        },
        set: function(newVal){
    
    
            if(val === newVal){
    
    
                return ;
            }
            val = newVal;
        }
    })
}

2.2 Collect dependencies

​ Collecting dependencies is to save the target data used in the template first, and when the data changes, trigger a rendering of the previously collected dependency loop.

​ To summarize in one sentence, the getter collects dependencies, and the setter triggers the dependencies .

​ Let's encapsulate the code that collects dependencies into classes, and improve the encapsulation content above to enable it to collect trigger dependencies.

class Dep{
    
    
    constructor(){
    
    
        this.subs = [];
    }
    
    addSub(sub){
    
    
        this.subs.push(sub);
    }
    
    removeSub(sub){
    
    
        let index = this.subs.indexOf(sub);
        if(index > -1){
    
    
            this.subs = this.subs.splice(index,1);
        }
    }
    depend(){
    
    
        if(window.target){
    
    
            this.addSub(window.target);
        }
    }
    notify(){
    
    
        let subs = this.subs.splice();
        for(let i=0;i<this.subs.length;i++){
    
    
            subs[i].update() 	// 这里的update方法在后续定义
        }
    }
}

function defineReactive(data,key,val){
    
    
    let dependencies = new Dep(); // 依赖
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get:function(){
    
    
            dependencies.depend();
            return val;
        },
        set: function(newVal){
    
    
            if(newVal === val){
    
    
                return ;
            }
            val = newVal;
            dependencies.notify();
        }
    })
}
  • In the above code, the dependency we collected is called window.target, in fact, it has an abstract name: Watcher .

2.3 Role of Watcher

​ Watcher plays the role of an intermediary in Vue, notifying him when there is a data change, and then notifying it to other places.

​ In Vue, how to use Watcher:

vm.$watch("name",function(newVal,oldVal){
	// do something
})

​ To implement a watcher, just add the watcher instance to the data.name attribute and point window.target to it. When the data changes, you can notify the Watcher, and the Watcher can execute the callback function in the parameter.

class Watcher{
    
    
    // vm,属性或函数,callback
    constructor(vm,expOrFn,cb){
    
    
        this.vm = vm;
        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 oldVal = this.value;
        this.value = this.get();
        //触发回调函数
        this.cb.call(this.vm,this.value,oldVal);
    }
}
  • In Watcher's get method, we point window.target to this, and in the process of obtaining the current dependent value (such as data.name), we set off the getter function;

  • After the getter is triggered, the dependent this will be stored in the Dep instance, and then whenever the data.name changes, the notify method of Dep will be triggered, and all dependent update methods will be triggered cyclically to realize the "push" process.

  • The method of parsing the string path in the code is implemented as follows:

  • const exp = /[^\w.$]/;
    function parsePath(path){
          
          
        if(exp.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;
        }
    }
    
  • Read the data through the loop layer by layer, and the final result is the target data.

2.4 How to detect all keys

​ In the above code, we can only detect one attribute in the data at a time, but our purpose is to detect all attributes (including sub-attributes), so we can encapsulate a class and set all attributes in the data It becomes a key that can detect changes.

class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
    	// 当值不为数组时,将其转化为可侦测的	
        if(!Array.isArray(value)){
    
    
            this.walk(value);
        }
    }
    
    walk(value){
    
    
        const keys = Object.keys(value);
        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 Observer(val)
    }
    let dep = new Dep();
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get:function(){
    
    
            dep.depend();
            return val;
        },
        set:function(newVal){
    
    
            if(newVal === val){
    
    
                return ;
            }
            val = newVal;
            dep.notify();
        }
    })
}

2.5 The entire change detection process

First, data first converts all data into detectable data through Observer;
when the outside world reads the data, Watcher will trigger the getter, and the Watcher of the data will be collected to Dep (collection dependency);
when the data changes, the setter will be triggered, The setter will notify Dep, and Dep will notify Watcher;
after receiving the notification, Watcher sends a notification to the outside world, and the outside world performs corresponding operations according to the changes in the data.

3. Summary

  • Change detection is to detect changes in data.
  • Object can be Object.definePropertyused to convert properties into detectable mode. The getter is triggered when the data is obtained, and the setter is triggered when the data is modified.
  • Collect dependencies through getters, and notify dependent data changes through setters.
  • To collect dependencies, we need a way to store dependencies, so a Dep class is encapsulated to add, delete, and notify dependencies.
  • The so-called dependency is Watcher, as long as Watcher triggers the getter, it will be collected in Dep. When the data changes, the list will be circularly dependent and notified one by one.
  • The principle of Watcher is to set itself to the global unique object, then read the data, trigger the getter, the getter will obtain the global object, that is, the current Watcher, and add it to the Dep. Watcher actively subscribes to any data changes in this way.
  • In order to detect changes in all data in the object, we encapsulate the Observer class, and convert all data (including sub-data) of the object into detectable data through defineReactive.
  • Before ES6, JS did not provide metaprogramming capabilities, so it was impossible to track the changes of new and deleted properties of objects.

Guess you like

Origin blog.csdn.net/yivisir/article/details/114379919