前言
Vue是参考mvvm模式设计的一套用于构建用户界面的渐进式框架,可以自底向上逐层应用,采用非侵入性的响应式系统。在修改数据时,视图也会跟着更新,开发过程中只需关注数据。
开发中的响应式:
1 | <template> |
1 | <script> |
页面属性num发生变化时,经历了
- 获取属性num
- 更新属性num值
- 计算total值,更新页面
数据发生变化后,页面会重新更新数据。
想要完成整个过程,需要:
- 侦测数据变化 (简称:数据劫持)
- 收集视图依赖数据 (简称:依赖收集)
- 数据变化,通知视图需要更新的部分 (简称:发布订阅模式)
数据劫持
Vue2.x版本中使用Object.defineProperty进行数据劫持。
Vue通过对象属性getter/setter监听数据变化,通过getter进行依赖收集,每个setter方法就是一个观察者,数据发生变化时通知订阅者更新视图。
1 | var data = { |
函数observe传入一个需要被追踪变化的对象data,遍历对象每个属性都使用defineReactive处理,实现侦测对象变化。
侦测Vue中的data数据。1
2
3
4
5
6
7
8function Vue(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if(this.$el) {
observer(this.$data);
}
}
只需要new Vue一个对象,就会将data中数据进行追踪变化。
需要注意的是Object.defineProperty有以下缺点:
- 无法检测对象属性的添加和删除
因为Vue通过Object.defineProperty将对象的key转化成getter/setter依赖追踪变化,而getter/setter只能追踪数据是否被修改,却无法追踪新增属性和删除属性。
对于新增属性,使用Vue.set()方法,可以将新增属性添加到Vue响应式系统中;如:在data对象下新增一个size属性,使用Vue.set(data, ‘size’, ‘10KB’),参数依次是:目标对象,目标对象新增属性,目标对象新增属性值。
也可以给这个对象重新赋值,如:Vue.set(data, ‘title’, ‘MVue’) 。
对于删除属性,使用Vue.delete(目标对象, 删除目标对象属性);如:Vue.delete(data, ‘obj’)。
- 不能监听数组变化,可以对数组方法进行重写(参考深入浅出Vue.js)
1 | var arr = ['小社区', '社区', '大社区']; |
上述把数组原自带的方法进行重写,覆盖掉原数组方法;重写后的数组方法需要被拦截,但是Vue对这些重写的方法是拦截不到的,也就不能响应。
比如:修改上述数组某一项值,无法侦测数组变化。
1 | arr[1] = '物业'; |
Vue3.x使用proxy作为实现代理,proxy具有代理、拦截与劫持的特征。
proxy实现特征:
1 | let arr = ['小社区', '社区', '大社区']; |
对比Object.defineProperty与proxy:
Object.defineProperty必须遍历对象每个属性;无法检测对象属性的新增属性与删除属性;无法监听重写数组方法的变化。
proxy只需做一层代理就能监听同级结构下所有属性,支持代理数组变化。(深层次的数据结构,还是需要递归)
收集依赖
观察数据目的是当数据属性发生变化时,可以通知那些使用了该数据的地方。
比如:开篇用到的数据num,当数据num发生变化时,会通知所有用到数据num的地方。
如果是多个Vue实例共用一个变量,比如:
1 | var str = 'Vue'; |
此时更改str属性值,这两个实例视图会更新。那么只有通过收集依赖才能知道哪些地方依赖了数据str,以及数据str派发更新数据。
收集依赖核心思想是事件发布订阅模式,这里有两个角色:订阅者Dep和观察者Watcher。
收集依赖是为依赖寻找一个存储依赖的地方,因此创建了Dep。使用订阅者Dep用来收集依赖,删除依赖、向依赖发送消息。
简单实现订阅者Dep:
1 | function Dep() { |
从上面代码订阅者Dep的作用是存储观察者Watcher,可以把观察者Watcher理解成一个中转站,当数据发生变化时通知观察者Watcher,再有观察者Watcher通知其他地方。
当需要依赖收集时调用函数addSub,当需要派发更新时调用函数notify。
1 | var dep = new Dep(); |
如何收集依赖:在getter中收集依赖,在setter中触发依赖;就是把用到该数据的地方收集起来,等到属性发生变化时,把之前收集好的依赖循环触发一边。
具体是当外界通过观察者Watcher读取数据时就会触发getter,将观察者Watcher添加到依赖中。哪个观察者Watcher触发getter就把哪个观察者Watcher收集到Dep中;当数据发生变化时,会循环依赖列表,把所有的观察者Watcher都通知一遍。
观察者Watcher
Vue官方定义一个Watcher类用来表示观察订阅依赖。其中《深入浅出Vue.js》给出这样的解释:为什么要引入观察者Watcher。
在属性发生变化后,需要通知用到该数据的地方。而该数据可能被很多地方用到,并且类型还不一样,可能是模版,可能是开发者编写的watch。这时候需要抽象出一个能集中处理这些情况的类,然后在依赖收集阶段只收集这个封装好的类的实例,通知也只通知这个封装好的类的实例,再由这个封装好的类的实例通知到其他。
依赖收集的目的是将观察者Watcher存放到当前订阅者Dep的subs中。
简单实现观察者Watcher:
1 | function Watcher(data, key, cb) { // cb ->callback缩写 |
执行构造函数把Dep.target指向自身,收集到对应的Watcher,在派发更新时取出对应的观察者Watcher,执行函数update。
总结
结合以上内容实现一个简单响应式
1 |
|
函数render被渲染时,读取所需对象的值,会触发getter方法把当前观察者Watcher收集到函数Dep中;如果需要修改对象的值,会触发setter方法,通知函数Dep中的notify方法,触发所有观察者Watcher对象中的update方法更新对应视图。
总结Vue响应式原理
通过数据劫持结合订阅与发布者模式的方式,通过Object.defineProperty劫持各个属性的getter/setter,在数据发生变化时发布消息给订阅者,触发相应的回调函数。
执行new Vue整个过程发生了:
new Vue后,Vue会调用函数_init进行初始化。在这个过程data通过函数observer转化成getter/setter追踪数据变化;当被设置的对象被读取时会执行getter方法,当对象被重新赋值时会执行setter方法。
函数render执行时,会读取所需对象的值,会触发getter方法把观察者Watcher添加到依赖中进行依赖收集。
修改对象的值时,会触发相应的setter方法;setter方法通知之前依赖收集得到的Dep中每一个观察者Watcher,再有观察者Watcher通知其他,自己的值被更改了需要重新渲染视图;这时观察者Watcher就会调用update方法更新视图。