Vue:Array变化侦测

Vue:Array变化侦测

1. 数组追踪变化

​ 与Object不同,数组无法通过getter和setter方式来追踪变化,因此,我们需要自定义一个拦截器来追踪变化。

2. 拦截器的准备

​ 拦截器其实就是一个与Array.prototype一样的Object,里面所包含的属性一样,但是改变数组的方法是我们修改过的。

​ 首先,我们先对Array.prototype来个定制。

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
    })
})
  • 上面代码中,我们对Array.prototype进行了拷贝,创建出新的对象arrayMethods,其中的mutator就是我们追踪变化的关键,未来将对其进行扩展。

3. 使用拦截器覆盖Array原型

​ 要将一个数据转化成响应式的数据,需要通过Observer,在Observer里使用拦截器覆盖那些即将被转换成响应式的Array类型数据:

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. 收集依赖

​ 我们先来回顾一下收集依赖的类Dep:

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();
        }
    }
}

​ 与Object不一样的是,Array的依赖保存在Observer中,原因是这样做既能在getter中访问到Observer实例,又能在拦截器中访问到Observer实例。

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

​ Array的收集依赖也是在defineReactive中收集的,当我们把依赖保存到Observer后,我们可以在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. 在拦截器中获取Observer实例

​ 由于Array拦截器是对原型的封装,因此可以在拦截器中访问到当前正在操作数组的this:

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;
    }
    .......
}

​ 通过上面的方式,我们就可以通过__ob__来拿到Observer实例了,进而拿到dep实例。

__ob__的作用还可以用来记录当前value是否被Observer转换成响应式数据。如果拥有ob属性,则说明他们是响应式的;如果没有,则通过new Observer将其转换成响应式的。

​ 由于拦截器是原型方法,所以可以通过this.__ob__来访问Observer实例。

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. 向数组依赖发送通知

​ 想要发送通知,必须得先获取依赖,然后直接调用依赖的发送通知方法:

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. 侦测数组中元素的变化

​ 在这之前我们侦测的变化,指的是数组自身的变化,包括新增或删除一个元素;那么,要如何侦测数组中每一项元素的改变呢?

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]);
        }
    }
    ......
}

​ 上面的代码中,我们对数组中每一项都执行了一遍new Observer,即全部元素都转化成响应式的。

8. 侦测新增元素

​ 要侦测新增元素,首先得获取新增元素,我们可以使用Observer来实现:

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. 总结

  • Array与Object的追踪方式不同。Array是通过方法来改变内容的,因此我们需要创建拦截器去覆盖数组原型的方式来追踪变化;

  • 为了不污染全局Array.prototype,我们在Observer只针对需要侦测变化的数组,使用ES6的__proto__属性来覆盖其原型方法;对于不支持ES6语法的浏览器,我们直接循环拦截器,将所有方法直接设置到数组身上来拦截数组原型的方法;

  • Array的依赖保存在Observer实例上;

  • 在Observer中,我们对数组每一项元素都做了侦测并印上了标记__ob__,并把this保存起来,目的是:一是标记数据已经被侦测化;二是可以很方便拿到__ob__,从而拿到保存在Observer的依赖。方便后续通知。

  • 对于新增数据,我们对当前操作数组的方法进行判断,如果为’push’,‘unshift’,‘splice’,我们就从参数中将数据提取出来,然后使用observeArray方法对其进行数据侦测。

猜你喜欢

转载自blog.csdn.net/yivisir/article/details/114437783
今日推荐