3-2-15-Vue.js 源码阅读-响应式原理-数组

响应式原理-数组

在看 observer 的时候,在它的 constructor 里面是对数组有特殊的响应式处理的,下面讲解 Vue 中有关数组的响应式处理。

在 observer 中通过 Array.isArray 这个方法判断当前处理的数据是否是一个数组,如果是数组进行数组相关处理。

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  // 初始化实例的 vmCount 为 0
  this.vmCount = 0
  // 将实例挂载到观察对象的 __ob__ 属性
  def(value, '__ob__', this)
  // 数组的响应式处理
  if (Array.isArray(value)) {
    // 浏览器兼容处理,判断浏览器是否支持对象的原型 __proto__ 属性
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    // 为数组的每一个对象创建一个 observer 实例
    this.observeArray(value)
  } else {
    // 遍历对象中的每一个属性,转换成 setter/getter
    this.walk(value)
  }
}

如果 value 是一个数组,通过下面的方法判断浏览器是否支持对象的原型对象,然后根据支持的不同,选择给数组实现响应式的方法。

// can we use __proto__?
// 判断当前执行环境是否支持对象的原型对象属性
export const hasProto = '__proto__' in {}

支持原型对象属性的环境

如果判断当前执行环境是支持 proto 这个属性的,执行 protoAugment(value, arrayMethods)

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

protoAugment 这个方法的作用很简单,就是将第一个参数的原型对象重写为 src 。具体到这里就是给当前的数组属性的原型对象属性改为 arrayMethods。

下面看传入的第二个参数 arrayMethods 如何做处理的。

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]
  def(arrayMethods, method, function mutator (...args) {
    // 执行数组的原始方法
    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
    }
    // 对插入的新元素,重新遍历数组元素设置为响应式数据
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 调用了修改数组的方法,调用数组的 ob 对象发送通知
    ob.dep.notify()
    return result
  })
})

可以看到 arrayMethods 是一个对象,这个对象集成了 Array 的原型,并且对 Array 原型上改变数组的实例方法进行了重写,使其在保证原有功能的基础上,还能自动的发送通知,更新视图。

不支持原型对象属性的环境

如果判断当前的执行环境不支持 proto 这个属性,调用 copyAugment 这个方法,对数组进行响应式处理。

/**
 * Augment a 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])
  }
}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

调用 copyAugment 这个方法传入的参数 arrayKeys 是一个通过 Object.getOwnPropertyNames 这个方法获取的 arrayMethods 的所有的属性 key 数组。

看 copyAugment 中的逻辑其实就是使用最简单粗暴的方式,直接通过循环的方式,将需要重写的数组的原型方法,给他们重新赋值更新,使其具备我们想要让他们有的功能。

最后执行的方法是 observeArray()

/**
  * Observe a list of Array items.
  */
// 对数组做响应式的处理
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

observeArray 就是通过循环将数组中的每一个元素都进行响应式的处理,转换成 setter/getter。在 observe 里面会对元素的类型进行判断如果是数组或者对象进行处理,否则不处理。

思考

<div id="app">
  {{ arr }}
div>

<script src="../../dist/vue.js">script>
<script>
  const vm = new Vue({
    el: '#app',
    data: {
      arr: [2, 3, 5]
    }
  });

  // vm.arr.push(8) 
  // vm.arr[0] = 100 
  // vm.arr.length = 0 
  
script>

对于上述的示例,只有执行 vm.arr.push(8) 才会触发视图的自动更新,原因就是在 Vue 对数组的响应式处理中,只是对更改数组的方法进行了重写,示器具备了自动发送通知的功能,而数组对象的属性并没有全部重写,所以调用他的 length 这些属性的更改不会触发视图的自动更新。

如果我们想修改数组中的某一个值,可以调用数组的 splice 这个方法,这个方法可以实现将数组中的某几个元素替换成我们想要的元素。并且这个方法在数组响应式处理中是做过特殊处理的,所以可以通过 dep 的 notify 方法去发送通知,调用 watcher 的 update 方更新视图。

你可能感兴趣的:(#,Part,3,·,Vue.js,框架源码与进阶)