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中所有的订阅者也包括我。当我收到这份通知的时候,我就会在所有我用到该信息的地方更新。