检测数组变化

236 阅读2分钟

问题背景

  1. 当利用索引直接设置一个项时。例如:vm.items[indexOfItem] = newValue
  2. 当修改数组的长度时,例如:vm.items.length = newLength

以上两种情况并不能检测到变动

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

为什么直接修改不能检测到变化,但是使用splice方法可以,为什么添加的对象可以变为响应式的

分析

  1. 在通过observe方法去观察对象的时候会实例化observer,在它的构造函数中是专门对数组做了处理,它的定义在src/core.observer/index.js中。
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

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

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

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

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    ob.dep.notify()
    return result
  })
})

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

总结

对于没有增加值的数组,手动触发了订阅者,从而进行了页面更改;对于增加了值的数组,还增加了一步,将新增的值变为响应式的。