Vue 源码分析 -- 数组响应式化

131 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

数组检测

通过 Object.defineProperty 方法重写 getter setter 的方式来实现数据响应式,无法监听到数组插入删除的变化,这也是使用 Object.defineProperty 方法进行数据监控的缺陷。在 Vue 源码中,通过重写数组方法的方式解决了这一问题。来看下具体实现

数组方法重写

Vue 在保留原数组功能的前提下,对数组进行额外的操作,也就重新定义了数组方法。

// 首先将数组原型保存下来,需要重写的方法都在数组原型上
const arrayProto = Array.prototype
// 创建一个继承数组原型的对象,该对象拥有数组原型上的所有方法。
export const arrayMethods = Object.create(arrayProto)

// 需要重写的数组方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

接下来,对 methodsToPatch 中列举的方法进行重写。

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
  })
})

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

重写数组方法的基本思路,就是创建一个继承数组原型的对象,这样该对象就拥有了数组原型上的所有方法,然后对该对象的方法进行重写, 在重写的方法内部加上我们需要进行的额外才做,并调用数组原型上原本的方法。这样就达到了扩展方法的目的,能够对数组的增删进行拦截。

当我们在执行数据方法添加或删除数据元素时,就会触发设置 mutator 方法。

为什么需要创建一个继承数组原型的对象? 因为这样才不会污染全局的数组原型,我们只需要对 Vue 实例的 data 选项中的数组进行拦截。

现在数组方法已经重写完成了,那么在我们访问数组方法时,如何才能不调用原生的数组方法, 而是调用我们重写的方法呢?

回到数组初始化的过程中,在实例化 Observer 时,看下对于 Array 类型的数据时怎么处理的。

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)
    }
  }
}

对数组的处理存在两个分支, hasProto 作为判断条件, 用来判断当前运行的环境,在对象的原型上是否存在 __proto__ 属性。来看两个分支分别对应执行的方法

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

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])
  }
}

当对象原型上存在 __proto__ 属性时,直接将我们重写方法后的对象赋值给数组原型;如果没有 __proto__ 属性,则通过代理的方式在数组上添加方法。 在执行完这一步之后,当我们访问数组方式,调用的就是我们重写的方法了。

依赖收集

在数据初始化阶段会利用 Object.defineProperty 进行数据访问的改写, 也就是数据响应式化。在访问数组元素时,同样回个 getter 方法所拦截。而对于数组,在数据拦截的过程中需要进行特殊处理,在 defineReactive 方法中,对于数组类型的数据,会调用 defineArray 方法实现数据拦截操作

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
    }
  })
}

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

dependArray 方法中会对数组中每一个元素进行依赖收集,如果数组中元素是数组类型,则进行递归处理

派发更新

当调用数组的方法添加或者删除元素是, setter 方法是无法进行拦截的,唯一可以进行拦截的地方就是之前我们重写的各种数组方法,

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
  })
})

在重写的数组房中,取出数组中的 __ob__ 属性,也就是 Observer 实例,调用 ob.dep.notify 方法,进行依赖的派发更新。 如果调用的方法往数组中添加了元素,则需要对新添加的元素进行响应式化。

class Observer{
  // 对新添加的数组元素进行响应化
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

总结

Vue 中,数组的增删无法通过 setter 进行依赖更新,所以在 Vue 中,重新定义了数组的常用方法。同时在访问数组元素是依旧触发 getter 方法来进行依赖收集;在改变数组时,通过触发重写的数组方法运行,进行依赖的派发更新