Vue.js 中的 $watch

github 源码
Observer, Watcher, vm 可谓 Vue 中比较重要的部分,检测数据变动后视图更新的重要环节。下面我们来看看 如何实现一个简单的 $watch 功能,当然Vue 中使用了很多优化手段,在本文中暂不一一讨论。

例子:

// 创建 vm
let vm = new Vue({
  data: 'a'
})

// 键路径
vm.$watch('a.b.c', function () {
  // 做点什么
})

先阐明在这个 demo 以及Vue 中,它们的关系:

vm 调用 $watch 后,首先调用 observe 函数 创建 Observer 实例观察数据,Observer 又创建 Dep , Dep 用来维护订阅者。然后创建 Watcher 实例提供 update 函数。一旦数据变动,就层层执行回调函数。

keynote

Observer和observe

递归调用 observe 函数创建 Observer。在创建 Observer 的过程中,使用 Object.defineProperty() 函数为其添加 get set 函数, 并创建 Dep 实例。

export function observe (val) {
  if (!val || typeof val !== 'object') {
    return
  }
  return  new Observer(val)
}
function defineReactive (obj, key, val) {
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  // 是否允许修改
  if (property && property.configurable === false) {
    return
  }

  // 获取定义好的 get set 函数
  var getter = property && property.get
  var setter = property && property.set

  var childOb =  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      var value = getter ? getter.call(obj) : val
      // 说明是 Watcher 初始化时获取的, 就添加订阅者
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        // if isArray do some....
      }
      return value
    },
    set: (newVal) => {
      var value = getter ? getter.call(obj) : val
      if (value === newVal) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb =  observe(newVal)
      dep.notify()
    }
  })
}

你可能会疑问 Dep.target 是个什么鬼?😳
答案是:Watcher, 我们接下来看

Dep

export default function Dep () {
  this.subs = []
}

// 就是你!!~
Dep.target = null

// 添加订阅者
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

// 添加依赖
Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

// 通知订阅者:要更新啦~
Dep.prototype.notify = function () {
  this.subs.forEach(sub => sub.update())
}

Watcher

为了给每个数据添加订阅者,我们想到的办法是在数据的 get 函数中, 但是 get 函数会调用很多次呀~。。。 肿么办?那就给 Dep 添加个参数 target

export default function Watcher (vm, expOrFn, cb) {
  this.cb = cb
  this.vm = vm
  this.expOrFn = expOrFn
  this.value = this.get()
}

Watcher.prototype.get = function () {
  Dep.target = this
  const value = this.vm._data[this.expOrFn]
  // 此时 target 有值,此时执行到了上面的 defineReactive 函数中 get 函数。就添加订阅者
  Dep.target = null
  // 为了不重复添加 就设置为 null
  return value
}

Vue Instance

在 Vue Instance 做得最多的事情就是初始化 State, 添加函数等等。

// Vue 实例
export default function Vue(options) {
  this.$options = options
  this._initState()
}

// 初始化State
Vue.prototype._initState = function () {
  let data = this._data = this.$options.data
  Object.keys(data).forEach(key => this._proxy(key))
  observe(data, this)
}

// $watch 函数,
Vue.prototype.$watch = function (expOrFn, fn, options) {
  new Watcher(this, expOrFn, fn)
}

总结

至此,我们已经实现了一个简单的 $watch 函数, Object.defineProperty() 函数可谓是举足轻重, 因此不支持该函数的浏览器, Vue 均不支持。

下一篇会实现对数据变动的检测。