Vue 源码剖析 —— 数组变化侦测

上一篇文章当中,我们剖析了对象的变化侦测。由于其侦测方式是通过 getter/setter 实现的,而当通过 array.pusharray.pop 等方法操纵数组时,是不会触发 getter/setter 的。

问题一:如何追踪数组变化

和追踪对象类似,我们的需求是在调用 array.push 等函数时能够收到通知。Vue.js 中是通过创建一个拦截器覆盖 Array.prototype。之后,当调用数组方法时,执行的将会是拦截器中的提供方法,我们也就能因此追踪到数组的变化。下面是一个基础拦截器的实现:

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

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach((method) => {
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    value: function mutator (...args) {
      return original.apply(this, args)
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
复制代码

在上面的代码中,我们创建了 arrayMethods 变量,它继承 Array.prototype。接着我们又在 arrayMethods 中使用了 Object.defineProperty。这样当我们调用 arrayMethods.push 时,其实是调用了其中的 mutator 函数,显然我们可以在 mutator 函数中添加对象变化侦测类似 setter 的逻辑。

我们不能直接让 arrayMethods 覆盖 Array.prototype,那会污染全局的 Array。我们的目的仅仅是拦截那些需要观测的数组,所以放在 Observer 类中处理会更为合理。

class Observer {
  constructor(value) {
    this.value = value

    if (Array.isArray(value)) {
      /**
      * 新增,注意不是所有浏览器都支持__proto__属性,Vue.js 源码在这段
      * 逻辑里面做了兼容处理,若不支持,则直接遍历 arrayMethods 
      * 将方法设置在被侦测的数组中
      **/
      value.__proto__ = arrayMethods
    } else {
      this.walk(value)
    }
  }
  ...
}
复制代码
问题二:如何收集依赖并向依赖发送通知

Object 的依赖是在 getter 中的使用 Dep 收集的,每个 key 都有一个对应的 Dep 列表来存储依赖。Array 的依赖和 Object 一样,也是在 defineReactive 中收集。

function defineReactive(data, key, val) {
  if (typeof val == 'object') {
    new Observer(val)
  }
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      // 这里收集 Array 的依赖
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}
复制代码

但此时我们需要改写一下 Dep 保存的地方,若按照之前的方式,对数组而言,我们只能在 getter 中访问到 dep,但在拦截器中是无法访问的,所以现在改写一下 Observer 类和 defineReactive

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods
    } else {
      this.walk(value)
    }
  }
  ...
}
function defineReactive (data, key, val) {
  let childob = observe(val) // 修改
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.depend()
      // 新增
      if (childOb) {
        childOb.dep.depend()
      }
      // 这里收集 Array 的依赖
      return val
    },
    set: function(newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}

function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceOf Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
复制代码

我们在 defineReactive 中调用了 observe,它把 val 作为参数输入,并返回一个 Observer 实例。这样我们就可以通过调用 childOb.dep.depend()getter 中添加依赖。接下来我们还需要修改一下 Observerob 属性到实例当中:

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    // 新增
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods
    } else {
      this.walk(value)
    }
  }
  ...
}

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
复制代码

我们已经可以通过数组数据的 ob 属性拿到 Observer 实例,然后就可以拿到 ob 上的 dep。现在我们就可以在拦截器中拿到依赖了:

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach((method) => {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    ob.dep.notify()
    return result
  })
})
复制代码

但是目前我们只是把数组变成了响应型的,数组项并没有被转换为响应式,需要在 Observer 内新增一些处理:

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    // 新增
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      this.observeArray(value)
      value.__proto__ = arrayMethods
    } else {
      this.walk(value)
    }
  }

  observeArray (items) {
    for (let item in items) {
      observe(item)
    }
  }
  ...
}
复制代码
问题三:如何侦听新增元素

当我们使用 push 或是 unshift 等方法添加元素时,新元素不是响应式的,所以我们要在拦截器中添加处理:

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach((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)
    ob.dep.notify()
    return result
  })
})
复制代码
问题四:Array 的遗留问题
  1. 当直接使用索引设置一个数组项时,比如 this.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:this.items.length = newLength 从上面的实现原理中可以判断,以上两种情况我们是没有办法监控到的。
Array 的变化侦测过程梳理

Array 是通过创建拦截器去覆盖数组原型的方式来追踪变化的,收集依赖的方式和 Object 一样,都是在 getter 中收集。但是由于依赖的使用位置不同,要在拦截器向依赖发送消息就必须能访问到依赖。所以依赖不能像 Object 那样保存在 defineReactive 中,而是把依赖保存在 Observer 实例上。所以我们在 Observer 实例中绑定了 ob 属性,并将 this 保存在 ob 上。

转载于:https://juejin.im/post/5d0a395551882563f967d8e1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值