第二章、Array的变化侦测

上一章我们讲到,vue对于对象的变化侦测是通过Object.defineProperty方法(实则是通过getter/setter)来实现的。本章我们来讲解vue对于数组的变化侦测是如何实现的。
Array不同于Object,我们在使用的时候其实是通过Array原型上的方法来改变数组的内容的,因此侦测Object变化的那种方式就行不通了。

侦测Array变化的拦截器

拦截器其实就是一个和Array.prototype一样的Object,里面的书香和数组原型的一模一样,只不过其中一些方法是经过我们处理的。

Array原型中可以改变数组自身内容的方法有7个:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

下面是我们创建的拦截器:

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

使用拦截器覆盖Array原型

现在有了拦截器之后,我们需要用它去覆盖数组的原型,但是我们希望它只是覆盖那些响应式数组的原型。
上一章我们说过,将一个数据转换成响应式的数据,需要通过Observer,因此,Observer做出如下改变:

export class Observer{
    constructor(value){
        this.value = value;
        if(Array.isArray(value)){
            value.__proto__ = arrayMethods;
        }else{
            this.walk(value);
        }
    }
    ...
}

原理如下图所示:
在这里插入图片描述

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

这里有个问题就是非标准属性__proto__属性在浏览器中的支持问题:
vue的做法是如果浏览器不支持__proto__这个属性,就直接将arrayMethods身上的这些方法设置到被侦测的数组实例上。

import {arrayMethods} from './array'

const hasProto = '__proto__' in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{
    constructor(value){
        this.value = value;
        if(Array.isArray(value)){
            const argument = hasProto? protoArgument: copyArgument;
            argument(value,arrayMethods,arrayKeys);
        }else{
            this.walk(value);
        }
    }
    ...
}

funtion protoArgument(target,src,keys){
    target.__proto__ = src;
}
function copyArgument(target,src,keys){
    for(let i=0,len=keys.length;i<len;i++){
        const key = keys[i];
        def(target,key,src[key]);
    }
}

如何收集依赖,依赖列表存在何处

数组其实也是在getter中收集依赖,在拦截器中触发依赖。
为什么呢?举个例子:

{
    list: [1,2,3,4]
}

要想得到上面这段代码中的list数组,那么你一定需要通过list这个key来读取,所以要在Object中通过key来读取数据就一定会触发这个key对应的getter

vue把Array的依赖存放在Observer中:
原因就是数组的依赖存放的位置必须能让getter和拦截器都能访问到。

export class Observer{
    constructor(val){
        this.value = value;
        this.dep = new Deep();
        if(Array.isArray(value)){
            const argument = hasProto? protoArgument: copyArgument;
            argument(value,arrayMethods,arrayKeys);
        }else{
            this.walk(value);
        }
    }
    ...
}

收集Array的依赖的代码(getter中访问Observer实例)如下:

function defineReactive(data,key,val){
    if(typeof val === 'object') new Observer(val);
    let childOb = observe(val); // 得到一个Observer实例
    let dep = new Deep();
    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;
        }
    })
}
export function observe(value,asRootData){
    if(!isObject(value)) return;
    let ob;
    // 避免重复侦测value变化,value.__ob__是Observer的实例
    if(hasOwn(value,'__proto__') && value.__ob__ instanceof Observer){
        ob = value.__ob__;
    }else{
        ob = new Observer(value);
    }
    return ob;
}

上面代码中出现了__ob__,大家可能比较疑惑它是什么,接下来给大家解惑。

扫描二维码关注公众号,回复: 8528155 查看本文章

在拦截器中如何访问Observer实例

由于Array拦截器是对原型的封装,所以它是可以访问到this的,因此我们需要在this上读取Observer实例。

// 工具函数
function def(obj,key,val,enumerable){
    Object.defineProperty(obj,key,{
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}
export class Observer{
    constructor(val){
        this.value = value;
        this.dep = new Deep();
        def(value,'__ob__',this);
        if(Array.isArray(value)){
            const argument = hasProto? protoArgument: copyArgument;
            argument(value,arrayMethods,arrayKeys);
        }else{
            this.walk(value);
        }
    }
    ...
}

因此,我们最初创建的数组拦截器就可以访问Observer实例并且触发依赖了:

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
;['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
    // 缓存原始方法
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods,method,{
        value: function mutator(...args){
            const ob = this.__ob__; // Observer实例
            ob.dep.notify(); // 触发依赖
            return original.apply(this,args);
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
})

侦测数组中元素的变化

现在Observer不光能处理Object类型的数据,还能将Array也转换成响应式的。

export class Observer{
    constructor(val){
        this.value = value;
        def(value,'__ob__',this);
        if(Array.isArray(value)){
            this.ovserveArray(value);
        }else{
            this.walk(value);
        }
    }
    ...
}
observerArray(list){
    for(let i=0,len=list.length;i<len;i++){
        observe(list[i]);
    }
}

侦测新增元素的变化

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
;['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
    // 缓存原始方法
    const original = arrayProto[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.observerArray(inserted); // 新增元素转换成响应式数据
        ob.dep.notify();
        return result;
    })
})

关于Array变化侦测存在的问题

  • 修改数组中第一个元素的值时(this.list[0] = 2),无法侦测到数组的变化
  • 清空数组的操作(this.list.length = 0),无法侦测到数组的变化
发布了247 篇原创文章 · 获赞 23 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/LiyangBai/article/details/103832500