检测变化的注意事项

    对于使用 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的,比如:

    但是添加新属性的场景我们在平时开发中会经常遇到,那么 Vue 为了解决这个问题,定义了一个全局 API Vue.set 方法,它在 src/core/global-api/index.js 中初始化:

    1. Vue.set = set

    这个 set 方法的定义在 src/core/observer/index.js 中:

    set 方法接收 3个参数,target 可能是数组或者是普通对象,key 代表的是数组的下标或者是对象的键值,val 代表添加的值。首先判断如果 target 是数组且 key 是一个合法的下标,则之前通过 splice 去添加进数组然后返回,这里的 splice 其实已经不仅仅是原生数组的 splice 了,稍后我会详细介绍数组的逻辑。接着又判断 key 已经存在于 target 中,则直接赋值返回,因为这样的变化是可以观测到了。接着再获取到 target.ob 并赋值给 ob,之前分析过它是在 Observer 的构造函数执行的时候初始化的,表示 Observer 的一个实例,如果它不存在,则说明 target 不是一个响应式的对象,则直接赋值并返回。最后通过 defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() 手动的触发依赖通知,还记得我们在给对象添加 getter 的时候有这么一段逻辑:

    1. export function defineReactive (
    2. obj: Object,
    3. key: string,
    4. val: any,
    5. customSetter?: ?Function,
    6. shallow?: boolean
    7. ) {
    8. // ...
    9. let childOb = !shallow && observe(val)
    10. Object.defineProperty(obj, key, {
    11. enumerable: true,
    12. configurable: true,
    13. get: function reactiveGetter () {
    14. if (Dep.target) {
    15. if (childOb) {
    16. childOb.dep.depend()
    17. if (Array.isArray(value)) {
    18. dependArray(value)
    19. }
    20. }
    21. }
    22. return value
    23. },
    24. // ...
    25. })
    26. }

    接着说一下数组的情况,Vue 也是不能检测到以下变动的数组:

    1.当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue

    2.当你修改数组的长度时,例如:vm.items.length = newLength

    对于第一种情况,可以使用:Vue.set(example1.items, indexOfItem, newValue);而对于第二种情况,可以使用 vm.items.splice(newLength)

    其实之前我们也分析过,在通过 observe 方法去观察对象的时候会实例化 Observer,在它的构造函数中是专门对数组做了处理,它的定义在 src/core/observer/index.js 中。

    这里我们只需要关注 value 是 Array 的情况,首先获取 augment,这里的 hasProto 实际上就是判断对象中是否存在 proto,如果存在则 augment 指向 protoAugment, 否则指向 copyAugment,来看一下这两个函数的定义:

    1. /**
    2. * the prototype chain using __proto__
    3. function protoAugment (target, src: Object, keys: any) {
    4. /* eslint-disable no-proto */
    5. target.__proto__ = src
    6. /* eslint-enable no-proto */
    7. }
    8. /**
    9. * Augment an target Object or Array by defining
    10. * hidden properties.
    11. */
    12. /* istanbul ignore next */
    13. function copyAugment (target: Object, src: Object, keys: Array<string>) {
    14. for (let i = 0, l = keys.length; i < l; i++) {
    15. const key = keys[i]
    16. def(target, key, src[key])
    17. }
    18. }

    protoAugment 方法是直接把 target.proto 原型直接修改为 src,而 copyAugment 方法是遍历 keys,通过 def,也就是 Object.defineProperty 去定义它自身的属性值。对于大部分现代浏览器都会走到 protoAugment,那么它实际上就把 value 的原型指向了 arrayMethodsarrayMethods 的定义在 src/core/observer/array.js 中:

    可以看到,arrayMethods 首先继承了 Array,然后对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写。重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知,这就很好地解释了之前的示例中调用 vm.items.splice(newLength) 方法可以检测到变化。