数据响应式原理 - 02

418 阅读4分钟

本文是关于数据响应式原理的学习笔记,目的在于更好的理解 Vue 的底层原理,篇幅较长,故而拆分为几篇,今后会陆续更新~

在上一篇《数据响应式原理 - 01》中,我们实现将一个对象变为响应式对象的处理,本篇则对如何处理数组进行介绍。

数组的响应式处理

目前为止,当 obj 的某个属性的值为数组时,我们只可以侦测到数组的查看,无法侦测到数组是否被修改,比如现在给 obj 添加 c 属性为数组如下:

let obj = {
  a: {
    m: {
      n: 1
    }
  },
  b: 2,
  c: [1, 2, { x: 3 }]
}
observe(obj)
obj.c.push(4)
console.log(obj)

得到的结果如下图,虽然 obj.c 是增加了一项,但并不能侦测到被修改这件事情。

image.png

如何侦测到数组被修改

思路分析

就是改写 obj.c.push 时的这个 push 方法,添加一个可以侦测的入口。借助的是原型链,新建一个 arrayMethods 对象 ,让 arrayMethods 隐式原型(__proto__) 指向 Array.prototype,在 arrayMethods 上重写数组方法,再让 obj.c(是个数组) 的 __proto__ 指向 arrayMethods ,那么 obj.c.push 时调用的 push 就是我们重写的 push,我们可以在重写的函数里进行侦测。
需要用到的方法:

  • Object.create()
  • Object.setPrototypeOf()

开始手写

新建 array.js 文件

// array.js
import { def } from './utils.js'

// 获取 Array.prototype,因为数组的方法都定义在这上面
const arrayPrototype = Array.prototype

// 让 arrayMethods 的隐式原型(__proto__)指向 Array.prototype
const arrayMethods = Object.create(arrayPrototype) 

// 7 种可以改变数组自身的方法
const methodsCouldChange = [
  'push',
  'pop',
  'unshift',
  'shift',
  'splice',
  'reserve',
  'sort'
]

// 改写上面 7 种数组方法
methodsCouldChange.forEach(item => {
  // 保留数组方法的功能
  const original = arrayPrototype[item]
  def(arrayMethods, item, function() { // 注意这里不能用箭头函数,因为 arguments 和 this 指向的原因
    // 能 console 就说明能侦测到改变了
    console.log('数组被改变了')
    // 给改写的方法添加上功能
    const result = original.apply(this, arguments)
    return result
  }, false)
})

export default arrayMethods

在 Observer.js 引入,现在文件如下:

// Observer.js
// ...同之前重复部分省略
import arrayMethods from './array.js'

export default class Observer {
  constructor(value) {
    if (Array.isArray(value)) { // 判断传入的对象是否是数组
      // 将数组 value 的隐式原型指向 arrayMethods
      Object.setPrototypeOf(value, arrayMethods)
    } else {
      this.walk(value)
    }
  }
}

至此,在 index.js 中 obj.c.push(4) 时,打印台的输出变为下图,可以看出已经能侦测数组的改变,并且 obj.c 也成功添加了 4 这一项:

image.png

给数组的每一项添加侦测

数组的某一项的值也可能为对象或数组,我们需要给它们也添加上侦测(添加 observeArray 方法) 完善 Observer.js 文件:

// Observer.js
// ...省略重复
import observe from './observe.js'

export default class Observer {
  constructor(value) {
    // ...
    if (Array.isArray(value)) {
      ...
      this.observeArray(value)
    } else {
      // ...
    }
  }
  ...
  // 处理数组,让数组的每一项变为响应式
  observeArray(arr) {
    arr.forEach(item => {
      observe(item)
    })
  }
}

对于数组新增项的侦测

在 7 种被改写的数组方法中,push,unshift,splice 这 3 种是可以增加数组的项的,新增的项也需要被侦测,所以需要完善 array.js 文件,增加如下代码:

// array.js
// ...省略重复代码
methodsCouldChange.forEach(item => {
  def(arrayMethods, item, function() {
    // 如果是 push、unshift、splice,则要判断有没有新增项
    let inserted = []
    switch (item){
      // 不同的 case 使用相同的代码
      case 'push':
      case 'unshift':
        inserted = [...arguments]
        break
      case 'splice':
      inserted = [...arguments].slice(2) // arguments 为类数组对象,不能直接调用数组方法
        break
      default:
        break
    }
    // 获取到 Observe 实例 ob,以便调用实例方法 observeArray
    const ob = this.__ob__
    if (inserted.length) {
      ob.observeArray(inserted)
    }
    return result
  }, false)
})

注意,这里是可以保证调用这些数组方法的数组身上是有 ob 属性的,因为 Observer.js 里的 def(value, '__ob__', this, false)

至此,我们已经完成了对数组的响应式处理,接下来就是对于依赖的收集和 Watcher 类的介绍,将在下篇继续分享~

One More Thing

类数组对象

上面的代码里有用到 arguments 对象,它是类数组对象(伪数组),对于一个普通的对象来说,如果它的所有属性名均为正整数,同时也有相应的 length 属性,那么虽然该对象并不是由 Array 构造函数所创建的,它依然呈现出数组的行为,在这种情况下,这些对象被称为“类数组对象”。
比如打印 arguments:

image (1).png

类数组对象的方法属性

  • 可以使用for循环
  • 类数组对象没有继承 Array.prototype,因此不能直接调用数组方。可以间接使用 Function.call 方法调用,如:Array.prototype.slice.call(类数组) 这样就可以调用数组方法了(用 [].slice.call(类数组) 也一样)

感谢.gif

点赞.png