Vue 数据绑定原理

394 阅读3分钟

这篇文章并不会一行一行的分析 vue 的源码,已经有很多文章都写过了,我会一步一步的讲清楚 vue 数据绑定的原理。

defineProperty

基本上每个人都知道 vue 是使用了 defineProperty 将属性转化为 getter/setter 来监听数据的改变。

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get: function() {
      console.log('invoke getter:' + val)
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      console.log('invoke setter:' + newVal)
      val = newVal
    },
  })
}
var a = {b: 1}
defineReactive(a,'b',1)
a.b          // invoke getter:1
a.b = 2      // invoke setter:2

可以看到,当获取 b 的值时,会触发 getter,给 b 赋值时,会触发 setter,这样,每次当我们写 this.b = 3 时,在 setter 里写上需要处理的动作,就可以达到响应式的目的。

通过这种方式,将 props 和 data 变成响应式的,但是这只是这两者的值变了,依赖它们的地方如何跟着改变呢?

为了达到这个目的,需要使用 Dep 类,在该类上定义了两个原型方法,用于向 subs 增加订阅者和通知订阅者更新。

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

Dep.prototype.depend = function () {
  if (Dep.target) {
    this.subs.push(Dep.target)
  }
}

Dep.prototype.notify = function () {
  // 先备份,避免 subs 改变
  const subs = this.subs.slice()
  subs.forEach(sub => {
    sub.update()
  })
}

这时,修改一下上面的 defineReactive 方法,给每个属性里增加一个 dep 实例,用于收集依赖,当有其它的变量调用了该属性的值时,就会在 subs 里增加一个订阅者,当该属性的值发生改变时,就会在 setter 里调用 notify,通知订阅者更新。

function defineReactive(data, key, val) {
  let dep = new Dep() // 新增
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get: function() {
      console.log('invoke getter:' + val)
      dep.depend() // 增加订阅者
      return val
    },
    set: function(newVal) {
      if (newVal === val) return
      console.log('invoke setter:' + newVal)
      dep.notify() // 通知更新
      val = newVal
    },
  })
}

那么问题来了,谁是订阅者,如何加订阅者添加到 subs 中?这时我们需要一个 Watcher 类

function Watcher(vm, exp, cb) {
  this.vm = vm
  this.exp = exp
  this.cb = cb
  this.value = this.get()
}

Watcher.prototype.update = function() {
  let oldVal  = this.value
  let newVal = this.get()
  if (oldVal !== newVal) {
    this.value = newVal
    this.cb.call(this.vm, newVal, oldVal)
  }
}

Watcher.prototype.get = function () {
  Dep.target = this
  let value = this.vm[this.exp]
  Dep.target = null
  return value
}

在 Vue 里,有三处实例化了这个类,分别是使用 computed、watch和模板渲染的时候,这个也很容易理解,因为只有这三处依赖了其它数据的变化而变化,因此需要使用 Sub 来收集 Watcher,当数据发生变化时,会执行 Sub 里的每一个 Watcher 的 update 方法,update 里执行回调。

举例来说明:

vm.$watch('a', function (newVal, oldVal) {
  // do something
})

这里 watch 了变量a,vue 里会 new 一个watcher对象 var watcher = new Watcher(vm, expOrFn, cb, options); 此时 expOrFn = 'a',cb为里面的回调函数。此时 Watcher 类里会首先执行 this.get(),即首先获取一次 data.a 的值(第19行),此时触发了 a 的 getter,上面分析到,会在 getter 里收集依赖,这里将 target 设为 this,即当前的 watcher 对象,getter 里会读取target,将该 watcher 对象推进 subs,这样就完成了依赖收集。接下来如果 a 发生变化,就会执行 setter 里的 dep.notify,会执行每一个watcher里的update,即执行了上面的cb。在模板渲染的时候也是类似,只不过cb换成了更新视图的方法,即

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

上面介绍了数据绑定的三个比较重要的点,接着就来总结一下整个流程

  1. 当 new Vue 对象的时候,options 里传入 data、computed、watch、template 等等
  2. 使用 defineReactive 处理 data 里的每一项,增加 getter 和 setter,检测到变量的变化
  3. 处理 computed 和 watch 选项,在对应的依赖变量的 sub 里注入 watcher,这些变量发生改变的时候,执行 update 方法,触发回调函数
  4. 解析模板,转化为 render 函数,构造 updateComponent 方法,然后 new 一个 Watcher 对象,执行该方法时,给使用到的变量注入依赖,这样变量更新的时候就触发 updateComponent 方法,更新视图