Vue.js中watch的实现

320 阅读2分钟
原文链接: www.xmxmxm.me

本文简述Vue.js中watch的实现,代码均为为叙述而简化的代码。

下面是常见的Vue代码,本文以此为例:

const vm = new Vue({
  data: {
    a: 1
  },
  watch: {
    a: function (val, oldVal) {
      console.log('a你变了')
    }
  }
})

setTimeout( () => {
  vm.a = 2
}, 2000)

因Vue实例中watch了a,所以两秒后a的值更改时,控制台将打印:'a你变了'。

Vue是如何做到这件事的呢?

当对Vue实例添加watch时,Vue会new一个Watcher类的对象即watcher,以下列举watcher对象的几个关键属性:

watcher: {
  // API中watch传入的回调函数,在Watcher.prototype.update方法中调用
  cb: function() {...},
  // expOrFn可理解为对象的key(例中{a: 1}的a)
  expOrFn: 'a',
  // 调用Watcher.prototype.get方法,用以记录旧值
  value: this.get()
}

上面this.get()调用的Watcher.prototype.get方法:

Watcher.prototype.get = function () {
  // 在下文添加订阅者的关键代码。Dep类稍后解释
  Dep.target = this
  // 获取数据对象的当前值(例中{a: 1}初始化调用时为1)
  const value = this.vm._data[this.expOrFn]
  // after get value, set null
  Dep.target = null
  return value
}

Vue在初始化时,先对data属性中的键值创建了观察者Observer对象。 在Watcher.prototype.get方法中,因为this.vm._data[this.expOrFn]代码对对象值进行了获取,所以调用了Observer中为data定义的getter,getter中添加了订阅者。
以下是Observe为对象添加getter,setter的代码:

const dep = new Dep()

Object.defineProperty({a: 1}, a, {
  enumerable: true,
  configurable: true,
  get: () => {
    // 在之前Watcher.prototype.get中设置了Dep.target
    if (Dep.target) {
      // 添加订阅者
      dep.depend()
    }
    return val
  },
  set: (newVal) => {
    // 如果新值和旧值一样则不作操作
    if (val === newVal) {
      return
    }
    val = newVal
    // 发送通知
    dep.notify()
  }
})

如上文所见,Dep类是维护watcher的一个类,它挂载了一个叫subs的数组,当调用dep.depend()时,就往dep.subs中push了watcher。当调用dep.notify()通知更新时,Dep会循环dep.subs中的watcher并调用watcher.update方法。

因此,当我们调用vm.a = 2时,Observe的setter会执行并发送通知,watcher.update方法会被触发。update方法就显而易见了:

Watcher.prototype.update = function (dep) {
  // 获取当前值即更新后的值,此时自然也添加了新的watcher
  const value = this.get()
  // 此watcher中存储的旧值
  const oldValue = this.value
  // 更新值
  this.value = value
  if (value !== this.value || value !== null) {
    // 传入的回调函数被调用,此时打印 'a你变了'
    this.cb.call(this.vm, value, oldValue)
    // 从dep.subs中删除此watcher
    dep.subs.shift()
  }
}

以上。