源码分析:Vue 响应式原理 数组(Array)的响应式

200 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue中数组如何响应式

我们使用vue开发的时候,将数据定义在data中,这样的数据就可以成为响应式数据,当定义的数据渲染在页面渲染的时候,我们通过改变数据的值,就能够触发页面的重新渲染。关于响应式的核心原理可以点击这里,我们了解了Vue的响应式核心是触发了observe函数,利用Object.defineProperty这个API给对象中的每个值定义了getter和setter。

但在observe函数如果传入响应式的值是数组的时候,实例化Observer时有一段逻辑,源码如下:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 * Observer类,它连接到每个被观察对象。附加之后,观察者将目标对象的属性键转换为getter/setter
收集依赖项并分发更新。
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // __ob__属性指向this
    def(value, '__ob__', this)
    // 判断值是否为数组类型
    if (Array.isArray(value)) {
      // 拿到augment方法
      const augment = hasProto
        ? protoAugment
        : copyAugment
      // 执行augment方法
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

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

可以看到如果定义的响应式数据是数组的话,会执行augment()函数,而如果数据是普通对象,则并没有。那为什么定义的数组类型的数据中要执行这段逻辑,目的是什么呢?接下来我们继续深入:

const augment = hasProto
        ? protoAugment
        : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)

hasProto定义在src/core/util/env.js中,作用是判断浏览器环境下对象中是否支持__proto__属性,根据是否支持分别赋值为protoAugment函数和copyAugment函数。

// can we use __proto__?
export const hasProto = '__proto__' in {}
/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 * 通过使用__proto__截取原型链来增强目标对象或数组
 */
function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  // 将target的__proto__属性指向src
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 * 通过定义隐藏属性来增强目标对象或数组。
 */
/* istanbul ignore next */
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__属性,利用不同的方法,将src中定义的方法赋给目标对象的属性上,那接下来我们分析下传入给augment的参数具体都是什么:


const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

augment(value, arrayMethods, arrayKeys)
// 其中value是定义响应式的数组
// arrayMethods定义在src/core/util/env.js
// arrayKeys取的是arrayMethods的key值,也就是下面的methodsToPatch数组中的值

// 缓存Array.prototype属性
const arrayProto = Array.prototype
// 以原型继承的方式创建arrayMethods对象
export const arrayMethods = Object.create(arrayProto)

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

/**
 * Intercept mutating methods and emit events
 */
// 遍历methodsToPatch中的method
methodsToPatch.forEach(function (method) {
  // cache original method
  // 将Array原型上的方法缓存
  const original = arrayProto[method]
  // 利用Object.defineProperty API重写Array上定义的部分方法
  def(arrayMethods, method, function mutator (...args) {
    // 首先执行原型Array原型方法计算出结果值
    const result = original.apply(this, args)
    // 拿到__ob__属性
    const ob = this.__ob__
    let inserted
    // 根据方法不同截取不同参数
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 截取后参数不为空值,调用ob上的observeArray方法,将数组中的值重新做响应式处理
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 通知更新
    ob.dep.notify()
    // 返回结果
    return result
  })
})

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

从以上的代码中可以看到,Vue的内部其实是改写了Array.prototype上的一些方法,包括('push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')。这些方法改写后,调用这些方法的时候就能够通知与数组相关的页面进行更新。而augment函数的作用也就是利用原型继承或自定义方法的形式将改写后的方法赋值给了响应式数组。

到此为止,我们清楚了在Vue中对数组的响应式做了哪些处理,以及这样做的目的是什么了。

总结

当我们将数组定义为响应式数组,会将数组原型上的部分方法进行改写,将改写后的方法利用原型继承或自定义方法的形式赋值给了响应式数组,当数组的调用这些方法的时候,就能够通知跟数组相关的页面进行更新。