vue的变化侦测-Array篇| 8月更文挑战

589 阅读4分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

vue的变化侦测-Array篇

上次我们说过Object数据的变化侦测。Object的变化侦测主要依赖的是Object原型上的方法Object.defineProperty来监听其中的get和set方法,从而在get中收集依赖,在set中通知依赖更新。 但是Array型的数据是没有Object.defineProperty这个方法的,所以我们无法像Object一样来侦听。

1. array型数据是怎么来进行变化侦测的?

其实变化侦测的机制还是不变的,都是在获取数据时收集依赖,然后数据变化时通知依赖更新。

2. 怎么收集依赖的?

还是在get中收集依赖,谁获取了这个数组,就在get中收集依赖。与对象的依赖收集机制是相同的。

3. 怎么监测到array型数据变化了?(怎么通知依赖更新?)

因为数组是没有Object.defineProperty方法的,所以就不能通过get、set来监测变化。但是数组如果变化了,那么必定是操作了数组,而JavaScript中提供的操作数组的方法就那么几种'push','pop','shift','unshift','splice','sort','reverse'

vue内部就是通过将这几个操作数组的方法进行重写,在重写的方法中调用原生的数组方法,这样就可以保证重写的方法和原生的方法具有相同的功能。还可以在重写的方法中就可以做一些其他的事情,比如通知依赖更新。

4. 数组方法拦截器

image.png

// 源码位置 src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 创建一个对象作为拦截器,Object.create 是创建一个新对象,新对象的__proto__是传入的第一个参数
// arrayMethods 就是数组方法拦截器
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method] // 原生方法
  def(arrayMethods, method, function mutator (...args) { // 给arrayMethods 添加一个 method属性,属性的值是后面的function
    const result = original.apply(this, args)
    const ob = this.__ob__ //Observer类实例
    ob.dep.notify()
    return result
  })
})

5. 这个拦截器是怎么使用的?

使用这个拦截器时很简单,只需要将数组实例的__proto__属性设置为这个拦截器即可,这样数组实例调用原生的方法比如push方法时就会调用arrayMethods 上的push,即我们重写的mutator 方法。

那具体是怎么挂载到__proto__上的,我们来看代码:

// 源码位于 src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) { // 检测浏览器 是否支持 __proto__
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) // 将数组中的所有元素都转化为可监测的响应式数据
    } else {
      this.walk(value)
    }
  }

/**
 * Object.getOwnPropertyNames
 * 返回值:数组类型:包含指定对象的自身拥有的枚举和不可枚举的属性名组成的数组
 */
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
['push', 'splice', '__proto__', 'length']

// 这个方法就是讲数组实例的 __proto__指向了arrayMethods
 function protoAugment (target, src: Object) {
  target.__proto__ = src
} 

//如果浏览器不支持__protp__,则调用此方法,将拦截器中重写的那几个方法循环加入到数组实例上
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])
  }
}

6. 深度侦测

在前文所有讲的Array型数据的变化侦测都仅仅说的是数组自身变化的侦测,比如给数组新增一个元素或删除数组中一个元素,而在Vue中,不论是Object型数据还是Array型数据所实现的数据变化侦测都是深度侦测,所谓深度侦测就是不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化。

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)   // 将数组中的所有元素都转化为可被侦测的响应式
    } else {
      this.walk(value)
    }
  }


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

7. 新增数组元素的侦测

对于数组 中已有的元素我们已经可以进行侦测了,但是如果数组元素中新push了一条数据,即新增了一条数据,我们也应该把这个新增的元素变成可侦测的。 这个实现也比较简单,我们只需要拿到新增的数据,然后调用Observe函数,将其转化为可侦测的就行。

向数组中新增元素的方法有3个,push、unshift、splice.我们可以通过监听这个三个方法,拿到新增的数据即可。

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__ //Observer类实例
    /**
     * 对于数组 新增元素的 监测
     */
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args // 对于push和unshift 传入的参数就是要 新增的元素
        break
      case 'splice':
        inserted = args.slice(2) // 对于splice 下标为2的参数就是新增的元素
        break
    }
    if (inserted) ob.observeArray(inserted) // observeArray 将新增的元素转化为响应式
    ob.dep.notify()
    return result
  })
})

8.总结

因为数组的变化侦测是通过拦截器拦截数组的操作方法实现的,所以我们平常使用数组的下标来操作数据就不会触发vue的响应式更新。

解决方法:不用下标操作数组,改为用数组方法。或者使用Vue.$set