Skip to content

数组的响应式处理

前面在响应式一节,关于数组的处理当时没有着重分析,在文件src/core/observer/index.js中,入口如下:

js
  constructor(value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 将实例挂载到观察对象的__ob__属性,不可枚举,只是用来记录observer对象
    def(value, '__ob__', this)
    // 数组的响应式处理
    if (Array.isArray(value)) {
      // 判断当前浏览器是否有对象原型
      // 修补会改变原数组的方法,当这些方法调用时,调用dep.notify,通知更新视图
      // 修补后的方法设置到数组对象的原型上
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        // 若不支持__proto__
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的对象元素创建一个observer实例,
      this.observeArray(value)
    } else {
      // 遍历对象中的每一个属性,转换何曾getter/setter
      this.walk(value)
    }
  }

  ...
  // protoAugment 就是将value的 原型 设置 为 arrayMethods
  function protoAugment(target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

具体来看一看arrayMethods中的处理,核心就是对会改变原数组的七个方法进行重写,因为原生的方法根本不知道数据响应式的存在。

js
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]
  // 给arrayMethods每个方法重新注入对应的方法,也就是def的第三个参数function,function中传入的参数是调用数组方法传入的参数
  def(arrayMethods, method, function mutator(...args) {
    // 执行数组的原始方法
    const result = original.apply(this, args)
    // 获取数组对象的observe对象
    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对象发送通知
    ob.dep.notify()
    return result
  })
})

从代码中可以看出,首先使用数组的原型对象创建了一个新的对象,之后遍历methodsToPatch数组,重写每个方法,然后给arrayMethods注入重写后的方法。对于这七个方法也要做判断,如果有新增元素,重新遍历数组每个元素并设置为响应式数据。最后调用数组的ob对象发送通知,即派发更新。

vue2 数组响应式的问题

vue2在处理数组的响应式时,只是遍历了数组的所有元素,将元素中是对象的转化为响应式对象,而并没有处理数组的属性,比如length属性和索引,也就是当直接修改它们值的时候,不会触发响应式,页面上不会更新。为什么不去处理呢,因为当元素过多,会有性能问题。但是修补后的几个方法是响应式的。 有三种情况要区分开来:

  • 当修改数组本身这个值,即保存在栈区的值发生变化时,会触发notify,如vm.arr = 100,因为它本身是响应式对象。
  • 修改数组属性(如通过索引修改值与修改数组length)变化,不会触发notify,因为这些属性都不是响应式对象。
  • 当通过这七个方法修改数组元素,由于重写了这七个方法,会触发数组对象的dep的notify。因为这些方法最后都触发了数组本身去更新

但是在vue3中不存在这样的问题,做了性能提升,vue3中可以监听数组的索引和length属性,这个之后也会分析,因为它和vue3响应式密切相关。