Vue深度学习系列(二)监测数组变化

450 阅读3分钟

一、问题发现

在之前对于对象属性的监测中,是用Object.defineProperty通过对象的setter、getter方法来监测对象属性的变化,从而通过相应的依赖关系再去做相关的处理

而对于数组方法来说,也就是操作Array.prototype上的方法并不会触发该属性的setter方法,因为这个属性没有进行赋值操作。

因此,当我们需要对数组值的变化进行监测时,需要对它调用的数组原型方法进行重写,使其能够引起属性的setter/getter方法,从而实现监听

重写数组原型方法指的是修改数组的原型指向,进而重写原型方法,至于新的原型指向哪里,看接下来的源码分析

二、源码分析

对数组常用方法进行重写,主要为以下七个方法

popshiftpushunshiftsortsplicereverse

关于怎么去重写,大致就是通过原型链来拦截对数组的操作,从而监测到操作数组的变化,使用函数劫持的方式重写数组方法

1.监听器Observer

if (Array.isArray(value)) { //1.判断数组
  if (hasProto) { // 2.判断是否支持原型链
     protoAugment(value, arrayMethods) 
  } else {
     copyAugment(value, arrayMethods, arrayKeys) // 如果没有原型链会走def方法添加__ob__属性
  }
  this.observeArray(value) //3.监测
} else {
  this.walk(value)
}
  • 过程 在以上部分Observer代码中,对于数组的操作大概如以下几点
  1. 判断value数据(假设是arr数组)是否为数组
  2. 是数组的情况下,判断是否支持原型链
  3. 在支持原型链的情况下,执行protoAugment函数

2.protoAugment

function protoAugment (target, src: Object) {
  target.__proto__ = src // 让目标的原型链指向src
}

该函数作用是:将arr.proto===Array.prototype重新修改为arr.proto===arrayMethods 也就是将监听数组的原型指向为自定义的原型对象arrMethods

3.arrayMethods

//继承原型链arrayMethods.proto === Array.protptype
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
 
const methodsToPatch = [
  'push','pop','shift', 'unshift','splice','sort','reverse'
]

methodsToPatch.forEach(function (method) {
//1. 遍历常用的7大方法进行重写方法
  const original = arrayProto[method] 
//2. 重新定义原型方法
  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) // 对插入的数据再次进行监测
    // observeArray是将数组遍历一遍
    ob.dep.notify() // 通知视图更新
    return result
  })
}

arrayMethods作用就是:

  1. 被监听数组arr调用的方法通过原型链的方式从Array.prototype上拿到。(能继承到Array.prototype本身的属性,也不会影响到数组本身其他属性的使用)
  2. 重新定义被调用的方法,使用def函数(对数组方法引起的数组变化进行监听) 调用def函数传递的参数值中,其中重点关注val,也就是mutator函数

mutator函数中返回值就是我们需要设置的新值,也就是我们想要的数据变价, 具体过程:

  1. 调用数组方法进而操作数组
  2. 根据实际调用的数组方法(switch)确定新增数组的数据,并对其有效数据进行再次监测
  3. 数据变化,通知视图的更新

4.def

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def函数作用:实际就是Object.defineProperty()

总结: 在vue中对于数组的监测,对数组方法的调用进行原型链重写,通过指向自己定义的数组原型方法,新的原型有着原来Array.prototype上的所有方法(不回影响到数组本身其他属性的使用),来实现对数组变化的监测

但是以上都是对数组方法引起的数组变化进行监测,而像下面这样是不能被监测到变化的,需要其他手段,vm.$set和splice

arr[1] = 1212//通过索引赋值
arr.length = 3;//直接修改长度