Vue 最独特的特性之一,是其非侵入性的响应式系统。
众所周知,Vue的数据双向绑定给前端工作人员带来了极大的便捷。响应式系统使得开发人员只需要关注数据而无需手动控制dom来操作视图。假设 total = x * y当数据 x 改变时,Vue会帮助我们更改视图中所有的 x 及 total 等。
那么在这个无比顺滑的过程中,Vue内部是如何做到的呢?
答案是:
- 数据拦截/数据代理
- 依赖收集
- 发布订阅
翻译成人话就是:
-
监听数据变化(假定为
x) -
收集页面中该数据的依赖(即收集
total这种会随着x的变化而变化的数据)在这里先介绍一个概念,也是初学者刚开始最为模糊的一个概念:依赖。你可以把它看作一个名词,忽略掉它中文中的指向性。在我们上述的例子中,
total是x的依赖,total也是y的依赖。x和y也可能有其他很多的依赖。 -
当数据变化时,通知视图修改所有相关数据(即修改
x与total)
那么,这个过程具体是如何实现的呢?
数据拦截/数据代理
在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实现了数据拦截/数据代理前置知识:
getter/setter的目的在于将对象的属性封装为方法,避免了直接访问属性所带来的安全性问题,所有获取数据或更改数据都要通过getter/setter方法来实现。这种方法在Java中很常见。详见这里Object.defineProperty能够在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。详见这里
注意点:
-
由于Vue在初始化实例的时候才执行
setter/getter,所以无法检测到对象属性的添加或删除,如何需要可以使用全局方法Vue.set -
由于 JavaScript 的限制,Vue 不能检测数组的变动
-
Vue3.0将要采用
proxy来代替Object.defineProperty -
当我们设置
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中所有的订阅者也包括我。当我收到这份通知的时候,我就会在所有我用到该信息的地方更新。