什么是变化侦测?
Vue.js会自动通过状态生成DOM,并将其输出到页面上显示出来,这个过程叫做渲染。
那么,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染,如何确定状态中发生了什么变化?
这里就用到了 “变化侦测”,它就是用来监听内部数据状态变化的。
变化侦测的分类?
变化侦测分为两种类型,一种是 “推”(push) ,一种是 “拉”(pull)。
目前我们的前端主流框架中,Angular 和 React 中的变化侦测都属于 “拉” 的模式,也就是说,当状态发生变化时,它们并不知道哪个状态变了,只知道有可能变了,然后会发出一个信号告诉框架,框架接收到信号后,会进行一个暴力比对来找出那些DOM节点需要重新渲染。
这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。
在Vue.js中的变化侦测属于 “推” 的过程,当状态发生变化时,Vue.js立刻就知道了,而且在一定程度上知道那些状态变化了。因此,在Vue.js中可以进行更细粒度的更新。
如何侦测一个对象的变化?
在JS中,如果想侦测一个对象的变化,有两种办法:
- 1.使用 Object.defineProperty
- 2.使用 ES6 的 Proxy
注: 由于ES6在浏览器中的支持度并不理想,在Vue2.0源码中,还是使用了Object.defineProperty来实现的,但使用Object.defineProperty来侦测变化会有很多缺陷,比如:
- 没办法直接侦测数组的变化
- 对于多层的对象,需要使用递归进行侦测,多层递归会影响性能
- 没办法对新增、删除的属性进行侦测
所以,在已发布的测试版Vue3.0的源码里,作者使用Proxy重写了这部分的代码。
这里主要说说Vue2.0源码中的数据侦测的实现,同时也是对学习过程中的一个知识梳理。Proxy的方式实现数据侦测待Vue3.0源码正式版发布后,再和小伙伴们做讨论。
=> 使用Object.defineProperty来实现数据追踪?
代码如下:
// 定义一个响应式数据,在此函数中追踪变化
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val
},
set: function(newVal) {
if(val === newVal) {
return
}
val = newVal
}
})
}
通过上方defineReactive函数的封装,就能实现每当从data的key中读取数据时,get函数被触发,每当往data的key中设置数据时,set函数被触发。
如何收集依赖?
我们之所以要观察数据,目的是当数据发生变化时,可以通知曾经使用了该数据的地方,进行更新、渲染,所以我们要先收集依赖。
收集依赖: 即把用到数据的地方收集起来,等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。
在Vue2.0中,模板使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部再通过虚拟DOM重新渲染。
总结下来就是:在getter中收集依赖,在setter中触发依赖。
=> 依赖是什么?
数据变化时,曾经使用了该数据的地方有很多,而且类型可能还不一样,既有可能是模板,也有可能是用户写的一个watch。所以,我们需要抽象出一个能集中处理这些情况的类。
然后,在收集依赖阶段,只收集这个封装好的类的实例进来,通知也只通知它一个,它再负责通知其他地方,这个抽象的东西(类),就叫做 Watcher .
=> 依赖收集在哪里?
现在已经明确了,在getter中收集依赖,假设依赖是一个函数,保存在window.target上,我们就可以把依赖收集的代码封装成一个Dep类,帮助我们管理依赖:
// 为了减少耦合,把收集依赖的代码封装成一个dep类,专门帮助我们管理依赖
export default class Dep {
constructo