响应式系统(三)

前言

这章节承上章节漏掉的数组观测、新增属性观测

正文

观测数组

我们回到Observer的这段代码

if (Array.isArray(value)) {
    const augment = hasProto
        ? protoAugment
        : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
} else {
    this.walk(value)
}

上章节我们走的是else,这次我们走if,即value是数组的情况
我们先判断当前环境是否支持__proto__,看情况分别使用protoAugment、copyAugment,将其赋值给augment

augment(value, arrayMethods, arrayKeys)

我们先搞清楚arrayMethods、arrayKeys分别是什么

export const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

溯源可知arrayMethods就是数组的原型对象,所以我们再看protoAugment、copyAugment

function protoAugment(target, src: Object, keys: any) {
    target.__proto__ = src
}
function copyAugment(target: Object, src: Object, keys: Array) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}

可见protoAugment就是将arrayMEthods赋值给value.__proto__,也就是将处理过的数组原型上的方法赋值给数组原型,也就是劫持下数组原型对象,这样子我们就可以在调用[].splice之类的方法时在不破坏原生的操作之后加上自己的一些操作
copyAugment就是在数组不支持__proto__时,那我们就需要遍历arrayKeys,然后使用defObject.defineProperty)逐项设置,这样子也可以达到类似的效果
这样子就能做到在调用.splice之类的方法时可以执行注入到原型上的逻辑

这只是修改了数组对象的原型对象指向,将其指向修改过的arrayMethods。也就是并不是所有的数组对象都会被劫持,只有被观测的数组对象才会被劫持

最后this.observeArray(value)

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

就是遍历数组,然后逐项观测即可

arrayMethods

接下来看看arrayMethodsArray.prototype做了什么改动

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]
methodsToPatch.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.observeArray(inserted)
        ob.dep.notify()
        return result
    })
})

首先我们取到数组原型对象,然后通过Object.create复制一份副本。因为劫持必然会对原本的做出改动,使用副本的话不会影响原本的
然后定义methodsToPatch变量存储会对数组做出改动的方法,因为若不会对数组做出改动就没有什么劫持的价值
以上是准备工作,接下来遍历methodsToPatch。首先取得该项原方法赋值给original,然后使用def覆盖在原型上的此方法,既然是劫持,就不好做些影响原本结果的情况,就比如push的结果,劫持完了的push也该和原本的一致

const result = original.apply(this, args)
// ...省略
return result

这个就是调用原方法得到操作结果,最后返回
接下来看我们注入的逻辑

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

首先取得这个数组对象对应的__ob__赋值给ob,然后我们试想下,这么些个方法里有几个可是会增加新元素的,新的值自然也是需要观测的,所以我们得拿到这部分新值。对于push、unshiftargs就是新增的元素,splice可新增也可删除,新增的话就是参数的第三项,所以取args.slice(2)。然后简单了,判断inserted存在的话就调用ob.observeArray(inserted),最后调用ob.dep.notify(),触发该数组对象上收集到的依赖

观测数组和观测对象为何要区分

我们可以看到数组和纯对象观测是不一样的,纯对象的话每个键值都Object.defineProperty处理过,而数组的话索引是没有被处理过的,这也就导致了数组的索引是非响应式的
这个在官网有提到

响应式系统(三)_第1张图片
注意事项

其实这里很多人看到会有 误区,也就是是不是 Object.defineProperty监测不到索引变动什么的,其实不是。看这个 issue8562
也就是其实完全可以当做纯对象处理,不过终究是

性能代价和获得的用户体验收益不成正比

Vue.set

上章节我们简单说了下新增属性原理,也就是Vue.set,即:

  • 将新属性值转为响应式
  • 触发新属性宿主对象收集到的依赖(__ob__)

新增我们根据这个思路来看看Vue.set源码

export function set(target: Array | Object, key: any, val: any): any {
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
    ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
            'Avoid adding reactive properties to a Vue instance or its root $data ' +
            'at runtime - declare it upfront in the data option.'
        )
        return val
    }
    if (!ob) {
        target[key] = val
        return val
    }
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}

首先判断下该宿主对象情况,不能是undefined、null、原始类型
然后判断下若是数组,而且key是有效地索引,那么直接用splice就行了
接下来这段有点门道,所以深究下

if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
}

这个if有俩条件,即keytarget或者其原型链上且不能在Object.prototype上,那么就直接修改值就行了。其实原本并非如此,详情看这issues/6845。原本仅仅是if (hasOwn(target, key))

class Model {
    constructor() {
        this.foo = '123'
        this._bar = null
    }
    get bar() {
        return this._bar;
    }
    set bar(newvalue) {
        this._bar = newvalue;
    }
}
data = new Model()

试想若是target、key分别是data、'bar'那么hasOwn(data, 'bar') === false'bar' in data && !('bar' in Object.prototype) === true
可见前者会当做新增属性,后者直接当做已有属性,直接修改即可,即触发set bar
最后代码到这了就必然是新增属性
首先就是简单的取下ob对象,然后就是揭示一个规矩:

  • 不能给Vue实例设置新属性
    这个就是可能出现覆盖情况
  • 不能给根data设置新属性
    这个有点讲究,其实呢是可以的,如demo4。它为什么不可以呢,我们知道initData里有对data实现了代理访问即proxy(vm, '_data', key)。也就是vm.a === vm._data.a。我们新增的自然也就没有这层代理,那么根数据新增属性自然也就不能vm.nVal这样子访问了。所以如例子所示,自行做了这个代理就可以啦
    然后要是ob不存在的话就说明这个target非响应式,简单设置即可
    最后就是defineReactive转化成响应式,并且ob.dep.notify()触发依赖更新
Vue.del
export function del(target: Array | Object, key: any) {
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
    ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.splice(key, 1)
        return
    }
    const ob = (target: any).__ob__
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
            'Avoid deleting properties on a Vue instance or its root $data ' +
            '- just set it to null.'
        )
        return
    }
    if (!hasOwn(target, key)) {
        return
    }
    delete target[key]
    if (!ob) {
        return
    }
    ob.dep.notify()
}

首先就是和Vue.set一样的判定以及数组情况下调用劫持过的数组方法处理还有Vue实例对象以及根data不能操作的限定
然后就是if (!hasOwn(target, key)) {,这个就是判定该对象上有没有该属性,没有的话自然就return。这里为什么不用和Vue.set里一样呢,这是因为delete操作只会在自身的属性上起作用,要删除原型链上的属性就得传入那个原型对象
最后就是删除该属性,判断下ob不在的话就return,因为不是响应式的自然不用触发更新,是的话就ob.dep.notify()触发依赖更新

你可能感兴趣的:(响应式系统(三))