Array的变化侦测

Array的变化侦测与Object的有所不同,前面Object侦测方式是通过getter/setter的方式实现的,但如果使用数组方法例如“push”等来改变数组,并不会触发getter/setter,因为可以使用数组上的原型方法来改变数组的内容,所以Object的getter/setter实现方式就行不通。

如何追踪变化

我们可以用自定义的方法去覆盖原生的原型方法,用一个拦截器覆盖Array.prototype,之后每次使用Array原型上的方法的时候,其实使用的是拦截器中提供的方法,然后在拦截器中使用原生的Array的原型方法去操作数组。

拦截器

拦截器其实就是一个和Array.prototype一样的Object,里面的属性一模一样,只是拦截器中的数组方法是处理过的,Array中可以改变数组自身内容的方法有7个,分别是push、pop、shift、unshift、splice、sort、reverse。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
['push',...].forEach(function(method){ //缓存原始方法
    const original = arrayProto[method]
    Object.definePrototype(arrayMethods,method,{
        value:function mutator(...args){
            return original.apply(this,args)
        }
        enumerable:false;
        writable:true;
        configurable:true;
    })
})

创建了变量arrayMethods,继承Array.prototype,具备其所有功能,然后之后要去覆盖原型方法,在arrayMethods上使用Object.defineProperty方法将哪些可以改变数组自身的方法进行了封装,其实在调用方法的时候,其实调用的是mutator函数,里面可以做一些其他事,例如来发送变化通知等等。

使用拦截器覆盖Array原型

想让拦截器中的方法生效就必须覆盖数组的原型,但是不能直接覆盖,这样会污染全局的Array,做拦截操作我们只想让对那些被侦测了变化的数据生效,只想让拦截器只覆盖那些响应式数组的原型。我们需要通过Observer将一个数据转换成响应式的,所以需要在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型数据的原型

export class Observer{
    constructor(value){
        this.value = value
        if(Array.isArray(value)){
            value.__proto__ = arrayMethods //覆盖原型
        }else{
            this.walk(value)
        }
    }
}

使用value.proto = arrayMethods来覆盖value原型的功能。

将拦截器方法挂载到数组的属性上

因为使用proto并不是标准方法,所以也需要处理不能使用它的情况,将arrayMethods身上的这些方法设置到被侦测的数组上
新增了hasProto来判断当前浏览器是否支持proto,如果不存在则调用新增的copyAugment函数,将已经加工了拦截操作的原型方法直接添加到value的属性中

function copyAugment(target,src,keys){
    for(let i=0,i<key.length,i++){
        const key = key[i]
        def(target,key,src[key])
    }
}

如何收集依赖

拦截器创建是为了当数组内容发生变化时会得到通知,通知谁?答案是Dep中的Watcher,数组收集依赖也是在getter中收集的,所以,Array在getter中收集依赖,在拦截器中触发依赖。

依赖列表存在哪?

Vue.js把Array的依赖存放在Observer中,数组的Dep(依赖)保存在Observer实例上,因为getter中可以方法Observer实例,Array拦截器中也可以访问到Observer实例

拦截器中获取Observer实例

Observer上在value上新增了一个不可枚举的属性ob,这个属性的只是当前Observer的实例,然后就可以通过数组数据的ob属性拿到Observer实例,然后就可以得到dep了
所有被侦测了变化的数据上都会有一个ob属性来标识它们是响应式的,如果是,则返回ob,否则,使用new Observer来讲数据转换成响应式数据

向数组的依赖发送通知

只需要在Observer中拿到dep属性,调用ob.dep.notify()去通知依赖发生了变化

侦测数组中的元素的变化

前面Observer中是将object的所有属性转换为getter/setter的形式来侦测变化的,Array也可以使用Observer来处理,也是一个递归的过程,将所有数组的子元素也转换成了响应式的

侦测新增元素的变化

对于新增的元素使用Observer来侦测,对拦截器中使用的方法的类型进行变化,如果是跟增加有关系的,则把参数中新增的参数拿过来使用Observer来侦测。也可以使用ob拿到Observer实例来使用ob.observeArray来侦测新增元素的变化。

关于Array的问题

Array的变化侦测是通过拦截原型的方式实现的,但是不通过数组方法对数组进行操作Vue.js是无法拦截的,例如

this.list[0]=2
this.list.length=0

总结

Array的追踪变化的方式和Object不同,因为它是通过方法来改变内容的,所以通过创建拦截器去覆盖数组原型的方法来追踪变化。
为了不污染全局Array.prototype,我们在Observer中只针对那些需要侦测变化的数组使用proto来覆盖原型方法,但是proto在ES6之前不是标准属性,不是所有的浏览器都支持。因此,对于不支持的浏览器,直接循环拦截器,把拦截器中的方法直接设置到数组身上来拦截原生的数组方法。
Array和Object收集以来的方式一样,都是在getter中收集,但是由于使用依赖的位置不同,数组要在拦截器中向以来发消息,所以不是保存在defineReactive中,而是把依赖保存在了Observer实例上
在Observer中,对每个侦测了变化的数据都标上了印记ob,并把this(Observer实例)保存在了ob上,主要是有俩作用,第一个是为了标记数据是否被侦测了变化,保证一个数据只被侦测了一次,另一方面可以很方便地通过数据取到ob,从而拿到实例上保存的Dep依赖。当拦截到数组发生变化时,向以来发送通知
除了侦测数组自身地变化外,数组元素发生的变化也要侦测,也是使用Observer中判断如果被侦测地数据是数组,则调用observeArray将数组中每个元素递归转换成响应式的。
除了侦测已有的数据外,当用户使用push等方法来向数组中新增数据时,新增的数据也要进行变化侦测,如果时push,unshift,splice方法,则从参数中将新增数据提取出来,使用observeArray对新增数据进行变化侦测
在ES6之前,JS没有提供元编程的能力,所以对于数组类型的数据,一些语法不能追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用length清空数组就无法拦截。