Vue: Array change detection

Vue: Array change detection

1. Array tracking changes

​ Unlike Object, arrays cannot track changes through getter and setter methods. Therefore, we need to customize an interceptor to track changes.

2. Preparation of the interceptor

​ The interceptor is actually an Object the same as Array.prototype, with the same properties contained in it, but the method of changing the array is modified by us.

​ First, let's customize Array.prototype first.

const arrayProto = Array.prototype;

const arrayMethods = Object.create(arrayProto);
let methods = ['push','pop','shift','unshift','splice','sort','reverse'];

methods.forEach(function(method){
    
    
    // 获取原始方法
    const original = arrayMethods[method];
    Object.defineProperty(arrayMethods,method,{
    
    
        value: function mutator(...args){
    
    
            return original.apply(this,args);
        },
        enumerable: true,
        writable: true,
        configurable: true
    })
})
  • In the above code, we copied Array.prototype and created a new object arrayMethods. The mutator is the key for us to track changes, and we will expand it in the future.

3. Override the Array prototype with interceptors

​ To convert a piece of data into responsive data, you need to use the Observer to use the interceptor in the Observer to cover the Array type data that will be converted into responsive data:

const hasProto = '__proto__' in {
    
    };	// 检测是否可用proto
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
        if(Array.isArray(value){
    
    
           // 根据是否支持ES6来对原型对象进行覆盖
           	const arguments = hasProto? 'protoArguments' : 'copyArguments';
           arguments(value,arrayMethods,arrayKeys);
        }else{
    
    
        	this.walk(value);    
        })
    }
    ......
}


function protoArguments(target,src,keys){
    
    
    target.__proto__ = src;
}
// 递归复制属性
function copyArguments(target,src,keys){
    
    
    for(let i=0;i<keys.length;i++){
    
    
        const key = keys[i];
        def(target,key,src[key]);	// 将arrayMethods的方法添加到target中
    }
}

4. Collect dependencies

​ Let's review the class Dep that collects dependencies first:

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<subs.length;i++){
    
    
            subs[i].update();
        }
    }
}

​ Unlike Object, the dependency of Array is stored in Observer. The reason is that it can access Observer instance in getter as well as Observer instance in interceptor.

class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
        
        this.dep = new Dep();
        
        if(Array.isArray(value)){
    
    
            // 这里不再赘述,直接用ES6语法
            value.__proto__ = arrayMethods;
        }else{
    
    
            this.walk(value);
        }
    }
    ......
}

​ The collection dependencies of Array are also collected in defineReactive. After we save the dependencies to Observer, we can collect them in the getter:

function defineReactive(data,key,val){
    
    
    let childOb = observe(val); //为val创建Observer实例
    let dep = new Dep();
    Object.defineProperty(data,key,{
    
    
        enumerable: true,
        configurable: true,
        get:function(){
    
    
            dep.depend();
            // 收集依赖
            if(childOb){
    
    
                childOb.dep.depend();
            }
            return val;
        },
        set:function(newVal){
    
    
            if(val === newVal){
    
    
                return ;
            }
            dep.notify();
            val = newVal;
        }
    })
}

/*
	为val创建一个Observer实例,创建成功返回新的Observer实例,若val已存在一个,则返回val
*/
function observe(val,asRootData){
    
    
    if(typeof val !== 'object'){
    
    
        return ;
    }
    let ob = null;
    if(hasOwn(val,'__ob__') && val.__ob__.instanceof Observer){
    
    
        ob = val.__ob__;
    }else{
    
    
        ob = new Observer(val);
    }
    return ob;
}

5. Get the Observer instance in the interceptor

​ Since the Array interceptor is a encapsulation of the prototype, you can access the this of the currently operating array in the interceptor:

function def(obj,key,val,enumerable){
    
    
    Object.defineProperty(obj,key,{
    
    
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}


class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
        this.dep = new Dep();
        def(value,'__ob__',this);	// 在value上新增一个不可枚举的属性__ob__,即当前Observer实例
        value.__proto__ = arrayMethods;
    }
    .......
}

​ Through the above method, we can __ob__get the Observer instance through here, and then get the dep instance.

__ob__The effect may also be used to record whether or not the current value is converted into a responsive Observer data. If they have the ob attribute, they are responsive; if not, they are converted to responsive via new Observer.

​ Since the interceptor is a prototype method, it can be this.__ob__used to access the Observer instance.

let methods = ['push','pop','shift','unshift','splice','sort','reverse'];

methods.forEach(function(method){
    
    
    // 获取原始方法
    const original = arrayMethods[method];
    Object.defineProperty(arrayMethods,method,{
    
    
        value: function mutator(...args){
    
    
            const ob = this.__ob__;
            return original.apply(this,args);
        },
        enumerable: true,
        writable: true,
        configurable: true
    })
})

6. Send notifications to array dependencies

​ If you want to send a notification, you must first get the dependency, and then directly call the dependency sending notification method:

let methods = ['push','pop','shift','unshift','splice','sort','reverse'];

methods.forEach(function(method){
    
    
    // 获取原始方法
    const original = arrayMethods[method];
    def(arrayMethods,method,function mutator(...args){
    
    
        const result = original.apply(this,args);
        const ob = this.__ob__;
        ob.dep.notify();	// 发送通知
        return result;
    })
})

7. Detect the changes of elements in the array

​ The change we detected before this refers to the change of the array itself, including adding or deleting an element; then, how to detect the change of each element in the array?

class Observer{
    
    
    constructor(value){
    
    
        this.value = value;
        def(value,'__ob__',this);
        
        if(Array.isArray(value)){
    
    
            this.observeArray(value);
        }else{
    
    
            this.walk(value);
        }
    }
    
    // 侦测数组内的每一项数据
    observeArray(items){
    
    
        for(let i=0;i<items.length;i++){
    
    
            observe(items[i]);
        }
    }
    ......
}

​ In the above code, we have executed new Observer for each item in the array, that is, all elements are transformed into responsive.

8. Detect new elements

​ To detect new elements, first get the new elements, we can use Observer to achieve:

let methods = ['push','pop','shift','unshift','splice','sort','reverse'];

methods.forEach(function(method){
    
    
    // 获取原始方法
    const original = arrayMethods[method];
    def(arrayMethods,method,function mutator(...args){
    
    
        const result = original.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(inserted) ob.observeArray(inserted);
        
        ob.dep.notify();	// 发送通知
        return result;
    })
})

9. Summary

  • Array and Object track differently. Array changes the content through methods, so we need to create interceptors to cover the array prototype to track changes;

  • In order not to pollute the global Array.prototype, we use ES6 __proto__properties to override its prototype method in Observer only for the arrays that need to be detected for changes ; for browsers that do not support ES6 syntax, we directly loop the interceptor and set all methods directly To the array body to intercept the method of the array prototype;

  • Array's dependencies are stored on the Observer instance;

  • In Observer, we detect and mark each element of the array __ob__, and save this. The purpose is: first, the marked data has been detected; second, it can be easily obtained __ob__, so Get the dependencies stored in Observer. Facilitate follow-up notifications.

  • For the new data, we judge the current method of operating the array. If it is'push','unshift','splice', we will extract the data from the parameters, and then use the observeArray method to detect the data.

Guess you like

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