Vue.js最独特的特性之一就是看起来并不显眼的响应式系统。 ---官方文档
而响应式系统最重要的组成部分就是变化侦测。
一、什么是变化侦测
简单来说,变化侦测的作用是侦测数据的变化,当数据变化时,会通知视图进行相应的更新。
Angular和React中的变化侦测属于“拉”,就是当状态变化时,不知道哪个状态变了,会发送一个信号给框架,框架收到信息后会找出那些DOM节点需要重新渲染。
Vue的变化侦测属于"推",当状态变化时,Vue立刻知道,可以进行更加细粒度的更新。
但是细粒度也是有代价的,粒度越细,绑定的依赖就越多,依赖追踪的成本也就越高(Vue1就是如此)。
所以,在Vue2就引入了虚拟DOM,将粒度调整为中等粒度,绑定的依赖不再是具体的DOM节点,而是一个组件,组件内部再用虚拟DOM进行对比
(这里加点个人看法,我觉得Vue在这个地方真的很巧妙,属于是中庸的思想,只追踪到需要渲染的组件,组件内部再用虚拟DOM,这样既有一定的细粒度,但是又至于很细,哈哈哈,总之就是高!)
二、如何追踪变化
那如何侦测数据(对象)的变化呢?
两种方法:Object.defineProperty和ES6的Proxy,由于ES6浏览器支持度并不理想,所以Vue2还是用的defineProperty。(现在Vue3就是使用的Proxy来实现变化侦测)
Vue2源码中用defineReactive来对Object.defineProperty进行封装(defineReactive源码其中先创建了一个dep实例,然后判断一个属性是不是configurable,再封装了getter和setter方法),去侦测一个数据的变化(也就是定义一个响应式数据),当数据被读取时,get触发,当修改数据时,set触发。
但是这里我要提一点:
Object.defineProperty是有它本身的缺陷的,这也导致了Vue2对于对象的变化侦测也是有着一定的缺陷的。如下:
1.Object.defineProperty只能侦测对象,不能侦测数组,这也是为什么只在Object的变化侦测中使用defineProperty。
2.Object.defineProperty只能侦测对象中的某个属性(get和set,也就是获取属性值和修改属性值),但是对于这个对象本身新增属性和删除属性是无法侦测到的。而Proxy是侦测整个对象本身,所以Proxy中提供的 handler.set() 是可以侦测到对象中新增的属性的。
所以vue2提供了两个api,vm.$set和vm.$delete来在对象中响应式的新增属性和删除属性
三、如何实现变化侦测呢?
我们知道了通过什么追踪变化,那么我们怎么实现当数据变化时,就通知视图作出相应的更新呢。
by the way,变化侦测都写在vue2源码的Observe类中
一句话:getter中收集依赖,setter中通知依赖。
那依赖又是什么呢?
Watcher!
Watcher是一个中介角色,数据发生变化时通知它,它再通知其他地方。
所以我们在getter中使用 dep.depend()收集依赖
在setter中使用dep.notify通知依赖
dep是Dep的实例,Dep是用来处理依赖封装的一个构造函数
所以总的来说
1.首先Watcher有一个全局的唯一的一个位置,
2.当读取这个数据时,就会触发Watcher的get(),Watcher就会通过pushTarget()将自己放到该位置,
3.然后调用这个数据的getter,就会执行dep.depend()收集依赖,从那个位置将触发该数据的Watcher放入依赖数组中,
4.然后当该数据改变时,就会触发该数据的setter,执行dep.notify()通知依赖,
5.dep.notify就会遍历依赖数组依次执行update(),
6.然后再执行queueWatcher()方法将该Watcher推送到异步执行队列中,在下一次事件循环中再让Watcher触发渲染(其实就是通过触发Watcher中的回调函数 updateComponent = () => { vm._update(vm._render(), hydrating) }触发渲染)。
这里我又提一点:
为什么要使用异步更新队列呢?
因为vue2使用watcher通知到组件,然后组件内部使用虚拟DOM,所以有可能两个变量都属于一个组件,然后他们都改变了,如果不使用异步更新队列,就要更新两次,实际上,我们只需要等所有状态更改完只修改一次就可以了。
所以当把Watcher加入到异步更新队列时,会判断它是否存在相同的Watcher,相同的话只留一个就ok了,这样一次事件循环只需要更新一次组件就ok了。