VUE源码解析之变化侦测(二)

10 篇文章 0 订阅
7 篇文章 0 订阅

  在VUE源码解析之变化侦测(一)中知道VUE中Object的变化是靠setter来追踪,只要一个数据发生变化,就会触发setter,进而遍历告知Dep里面的Watcher,Watcher再进一步进行相应的处理。接下来探讨下VUE中的Array侦测。
 

VUE中Array的数据侦测

array和object大有不同,如下例子:

this.list.push("binguo")

  当我们时候list方法的时候,根本没有触发到getter/setter方法,所以我们得通过第二方法侦测。你可能会想到了,可不可以看它有没有触发push这个方法?但可惜ES6之前,JS并没有提供元编程的能力,也就是没有提供可以拦截原生的原型方法。但难不倒尢雨溪大大,我们可以通过拦截器来实现。
 

拦截器

  其实理解起来也很简单,就是在Array使用push方法时,将其拦截进行相应操作。我们可以实现一个这样的拦截器与Array.prototype有一样的Object,里面包含的属性也一模一样,只不过这个object中某些改变数组自身的方法(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
    })
})

  上述代码可以看出,我们创建了一个arrayMethods方法,它继承了所有Array.prototype的属性,接下来,使用Object.defineProperty对改变数组自身的方法进行封装。这样,当我们调用arrayMethods.push时,其实调用的是mutator方法,在方法的最后才执行original(其是原生Array.prototype上的方法)原来的方法,比如push功能。
  想要让拦截器生效,就要去覆盖原来的Array.prototype。但又不能直接覆盖,因为这样做会污染全局变量。所以我们只需要覆盖那些需要侦测的数组就可以了,也就是Observer中的属性。

export class Obsrver{
    constructor(value) {
        this.value = value
        
        if (Array.isArray(value)) {
            value.__proto__=arrayMethods  //将其原型指向arrayMethods
        } else {
            this.walk(value)
        }
    }
}

  从上面代码看理解起来其实很简单的,当我们遍历侦测属性时,如果其是Array类型,就将其的原型指向arrayMethods即可。如果浏览器不支持__proto__的话,我们需要将arrayMethods身上的这些方法设置到背侦测的数组上,这样,当数组访问的时候就直接访问数据上的方法,不会继续访问原型的方法。如下代码:

import {arrayMethods} from "./array";
const hasProto = '__proto__' in {}//判断__proto__是否可用
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Obsrver {
    constructor(value) {
        this.value = value

        if (Array.isArray(value)) {
            value.__proto__ = arrayMethods //将其原型指向arrayMethods
            constructor(value)
            if (Array.isArray(value)) {
                const augment = hasProto ? protoAugment : copyAugment
                augment(value,arrayMethods,arrayKeys)
            }
        } else {
            this.walk(value)
        }
    }
    ......
}
function protoAugment(target, src, keys) {
    target.__proto__ = src
}
function copyAugment(target, src, keys) {
    for (let i = 0, l = keys.length; i < l; i++){
        const key = key[i]
        def(target,key,src[key]) //该函数作用是:在target上添加key属性,value值为src[key]。
    }
}

我们现在已经知道如何进行数组的侦听,接下来,就是要做的是当数组变化时通知谁,也就是收集依赖!
 

收集依赖

其实收集依赖也很容易理解,因为每次访问数组的时候,首先都会访问其数组名。因为数组名也是一个key,所以,我们只需要和Object一样,在defineReactive中收集即可。

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()  //收集Array的依赖
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return 
            }
            dep.notify()  //触发依赖
            val = newVal
        }
    })
}

但Vue中Array的Dep缺不是放在Observer中,而是放在Observer中,

export class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep() //新增dep
        def(value,'__ob__',this) //value上添加属性__ob__
        if (Array.isArray(value)) {
            value.__proto__ = arrayMethods //将其原型指向arrayMethods
            constructor(value)
            if (Array.isArray(value)) {
                const augment = hasProto ? protoAugment : copyAugment
                augment(value,arrayMethods,arrayKeys)
            }
        } else {
            this.walk(value)
        }
    }
}

这里可能会问为什么要放在Observer中?我们要知道依赖保存的位置很关键,它必须在getter和拦截器中可以访问到。
在上面可以看到下面这段代码:

def(value,'__ob__',this) //value上添加属性__ob__

其中value则是传进来的Array,在其身上添加一个__ob__的属性,值为当前的Observer。因为Array拦截器是对原型的一种封装,所以在拦截器中可以访问到this(当前正被操作的数组)。所以也可以借此通过this.__ob__来访问Observer进而可以拿到里面的Dep。这里我们在相应的defineReactice修改一下!

function defineReactive(data, key, val) {

    let childOb = observe(val) //修改
    let dep = new dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()

            if (childOb) { //是否Observer实例
                childOb.dep.depend();
            }
            return val
        },
        set: function (newVal) {
            ......
        }
    })
}
//尝试为value创建一个Obserrver实例,
//如果创建成功,直接返回新创建的Observer实例。
//如果已经存在一个Observer实例,则返回它
export function observe(value, asRootDate) {
    if (!isObject(value)) {
        return
    }
    let ob
    if (hasOwn(value, '__ob__' && value.__ob__ instanceof Observer)) {
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

上面的可以看到,如果value是相应数据,即含有__ob__属性就不需要再创建Observer实例,直接返回已创建的Observer即可,避免重复侦测value变化的问题。这样,当value身上标记了__ob__之后,拦截器也顺理成章可以访问到Observer的实例了。

['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__.Dep//获取Observer中的收集器
            ob.dep.nitify() //向依赖发送信息
            return original.apply(this, args)
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
})

这已经完了?还没有,这仅仅是给第一次数组添加了侦听。
 

子数据侦测

无论是object还是array我们都需要对所有响应数据的子数据进行侦测。所以我们还需要遍历一下:

export class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep() //新增dep
        def(value, '__ob__', this) //value上添加属性__ob__
        if (Array.isArray(value)) {
            this.observeArray(value)
            value.__proto__ = arrayMethods //将其原型指向arrayMethods
            constructor(value)
            if (Array.isArray(value)) {
                const augment = hasProto ? protoAugment : copyAugment
                augment(value, arrayMethods, arrayKeys)
            }
        } else {
            this.walk(value)
        }

    }
    observeArray(items) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
}

这仅仅是遍历了侦测原有的数据,如果是通过push网数据新增元素,这个新增的元素也是要侦测的,其实思路挺简单,我们只需要知道哪些是新增的属性,再对其进行侦测就好。数组中新增的数据的方法有push、unshift和splice,我们只需要进行判断,再把新增的元素取出来,如下:

['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__.Dep
            let addData
            switch(method){  //判断方法类型
                case 'push':
                case 'unshift':
                    addData = args
                    break
                case 'splice':
                    addData = args.slice(2)
                    break
            }
            if(addData) this.ob.observeArray(inserted) //新增侦测
            ob.dep.nitify() 
            return original.apply(this, args)
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
})

如上代码,我们只需要获取到添加的元素,然后通过调用Observer中的observerArray方法添加侦测即可。
 

Vue中Array侦测存在的一些问题

当我们修改的时候,我们只能通过调用其拦截器中的方法才会被侦测到,如果我们像下面那样

  this.list[2]="binguo"

我们会发现是侦测不到的,需要this.list数据更新了,但视图仍然是旧数据,无法侦测到数据的变化,又例如

  this.list.length = 0

这个清空数组操作也是如此,需要手动刷新或者进行一次重新赋值才会发送变化,这是ES5无法实现的,但在ES6利用proxy可以实现这些侦测,所以在Vue2.0我们需使用官网提供的vm.$set对数组进行这些操作。

 
注:本文主要参考自《深入浅出Vue.js》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值