Vue 最独特的特性之一,是其非侵入性的响应式系统。

众所周知,Vue的数据双向绑定给前端工作人员带来了极大的便捷。响应式系统使得开发人员只需要关注数据而无需手动控制dom来操作视图。假设 total = x * y当数据 x 改变时,Vue会帮助我们更改视图中所有的 xtotal 等。

那么在这个无比顺滑的过程中,Vue内部是如何做到的呢?

答案是:

  1. 数据拦截/数据代理
  2. 依赖收集
  3. 发布订阅

翻译成人话就是:

  1. 监听数据变化(假定为 x
  2. 收集页面中该数据的依赖(即收集 total 这种会随着 x 的变化而变化的数据)
    在这里先介绍一个概念,也是初学者刚开始最为模糊的一个概念:依赖。你可以把它看作一个名词,忽略掉它中文中的指向性。在我们上述的例子中, totalx 的依赖,total 也是 y 的依赖。xy 也可能有其他很多的依赖。
  3. 当数据变化时,通知视图修改所有相关数据(即修改 xtotal

那么,这个过程具体是如何实现的呢?

数据拦截/数据代理

在JavaScript中侦察到一个对象的变化,有两种方法: Object.defineProperty ,ES6中的 proxy

  • Object.defineProperty当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性(如果属性仍是一个对象则递归这个过程),并使用 Object.defineProperty 把这些属性全部转为 getter/setter乍听起来,设置属性为getter/setter好像并没有特殊之处,但是仔细想想,每次获取数据,修改数据都需要经过这个方法,在这个方法中我们就可以做很多我们想要的操作了。比如在setter/getter方法中alert一个 big brother is watch you。即,getter/setter实现了数据拦截/数据代理
    前置知识:
  1. getter/setter 的目的在于将对象的属性封装为方法,避免了直接访问属性所带来的安全性问题,所有获取数据或更改数据都要通过getter/setter方法来实现。这种方法在Java中很常见。详见这里
  2. Object.defineProperty能够在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。详见这里

注意点:

  1. 由于Vue在初始化实例的时候才执行 setter/getter ,所以无法检测到对象属性的添加或删除,如何需要可以使用全局方法 Vue.set
  2. 由于 JavaScript 的限制,Vue 不能检测数组的变动
  3. Vue3.0将要采用 proxy 来代替 Object.defineProperty
  4. 当我们设置 vm.someDate = value 时,视图并不会立马进行渲染。这样的后果是:如果在重新赋值后取值,可能会取到渲染前的值。解决:使用 Vue.nextTick(callback) ,在回调函数中执行对更新后的dom的操作 。
  • proxy相较于Object.defineProperty遍历对象的每个属性的做法,proxy只需要做一层代理就可以监听同级结构下的所有属性变化

依赖收集

如何进行依赖收集?如何找出 x 的所有依赖 total

首先介绍两个概念: Dep , watcher 。

Dep :观察者容器,即存储观察者的地方。每一个具有响应式的属性都具有一个 dep 实例,里面存放着观察者对象。

Watcher : 观察者,在观察者类内部中包含一个将自己加入到dep的方法与一个更新方法。

每个响应式数据都有自己的一个 dep 实例,由于上一步 getter/setter 的存在,当 total 需要使用 x 的时候,调用 x 触发 getter,在 getter 中会调用 dep.depend 以收集观察者,即将 total(依赖)的watcher添加到 x 的订阅者数组中。当 x 更新后,由于上一步 getter/setter 的存在,在setter中调用 dep.notify 以通知所有保存的 watcher 去更新数据及视图。

整个过程即为发布订阅。

总结:

假设一个场景,我们所订阅的每一份信息都可以看作是一个响应式属性,该信息被很多人订阅。所以该信息自己保存着一个 dep。当我需要这份信息的时候,获取该信息并触发getter。该信息就将我加入它的dep中,即 dep 里面保留着各种像我一样的订阅者。当该信息更新后,它通知dep中所有的订阅者也包括我。当我收到这份通知的时候,我就会在所有我用到该信息的地方更新。