Vue内部原理(三)| 小册免费学

110 阅读3分钟

JavaScript中Array.prototype上有很多常用的方法,MDN上也给出了介绍,虽然方法众多,但是可以改变数组内容的只有7个(fill和copyWithin处于试验阶段,所以源码中没有适配),分别是:pop、push、shift、unshift、splice、sort、reverse。

这里的主要内容放在代码注释中,能够看得更清楚

Vue处理数组响应式的办法是劫持数组原型方法,将数组元素的原型替换为重写的数组原型方法

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 列举出所有可以改变数组内容的方法名
const methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methods.forEach(function (method) {
  // 原来数组的方法
  const original = arrayProto[method]
  // def工具,为对象设置函数属性
  def(arrayMethods, method, function mutator (...args) {
    // 执行原方法,指定this
    const result = original.apply(this, args)
    // 每个属性都维护一个子的观察者,后面会说到
    const ob = this.__ob__
    let inserted
    // 处理新加入的参数
    switch (method) {
      case 'push':
      case 'unshift':
        // push和unshift都会在这里处理,获取参数,参数也需要做响应式处理
        inserted = args
        break
      case 'splice':
        // splice的新加的参数是从第三位开始的,如arr.splice(2, 1, 'bar')
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 通知更新,后面依赖收集会说到
    ob.dep.notify()
    // 将原方法执行的结果返回
    return result
  })
})

// util.js
export function def(obj, key, value, enumerable = false) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  })
}

上面这段代码有点长,来总结一下它的主要功能

  1. 开始先使用Object.create创建一个新的对象,原型是Array.prototype

  2. 然后列举能够改变数组的7个方法,遍历列举出来的数组方法,给创建出来的对象添加函数属性,期间执行原方法获取返回值,如果方法是可以传参的那就要对参数再进行观测,

  3. 然后通知视图更新,最后返回函数的返回值

现在具有响应式功能的数组方法已经重写完了,下一步就是将重写过的数组方法挂载到数组上了,这时候要将之前的Observer稍加改造,对要坚挺的数据进行类型检测,如果是数组元素就将重写的数组方法挂载到原型上

export default class Observer {
  constructor(value) {
    this.value = value
    // 用于维护依赖,后面会说
    this.dep = new Dep()
    def(value, '__ob__', this)
    // 类型检测
    if (Array.isArray(value)) {
      // 使用setPrototypeOf可以将第二个参数作为第一个参数__proto__
      Object.setPrototypeOf(value, arrayMethods)
      this.observeArray(value)
    } else {
      // 之前有observe的过滤,能够到这里的不是数组就是对象
      this.walk(value)
    }
  }

  walk(obj) {
    Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
  }
  // 将数组元素遍历观察
  observeArray(arr) {
    arr.forEach((i) => observe(i))
  }
}

到这里数据观察过程已经基本了解了,但是这里并不是最终版,这里可以在数据发生变化时通过setter来更新视图,但是如果数据并没有被页面引用呢,即便是引用了,任何一个数据变化都会导致页面更新,代价太大了,这就需要我们后面要说的依赖收集了。

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情