第2章 (第①篇 变化侦测)Object的变化侦测
开篇介绍
Vue.js最独特的特性之一就是看起来并不显眼的响应式系统,数据模型仅仅是一个普通的js对象。从状态生成DOM,在输出到用户显示的界面的一系列流程叫做渲染,应用在运行过程中会不断的进行重新渲染,响应式系统赋予了框架重新渲染的能力,其中重要组成部分就是变化侦测,变化侦测是响应式系统的核心,简单来说,变化侦测的作用就是侦测数据的变化。当数据变化时,会通知视图进行相对应的更新变化。
object的变化侦测
什么是变化侦测?
通常,在运行时应用内部的状态会不断进行变化,需要重新不停的进行渲染,如何确定是否状态发生了变化成为问题。变化侦测就是用来解决这个问题的,分为了两种类型,一种是“推”(push),一种是“拉”(pull).
Angular和React的变化侦测都属于“拉”,拉的意思就是当状态发生变化时,它不知到底时哪个变了,只是知道状态有可能变了,然后他会发送一个信号来通知框架,框架接收到信号后通过暴力比对来找出哪些DOM节点需要重新渲染,这在Angular中是“脏检查”的流程,在React中使用的是虚拟DOM。
Vue的变化侦测属于“推”,推的意思是当状态发生变化时,Vue.js立马就知道了,并且在一定程度上知道是哪些状态变了,因此,它知道的更多,所以可以进行更细粒度的更新,细粒度指的是假如有一个状态绑定着好多个依赖,每个依赖表示一个具体的DOM节点,那么这个状态发生变化时,会向这个状态所绑定的所有依赖发送通知,让他们进行DOM更新操作。但是细粒度的更新也会有一定的代价作为补偿,粒度越细,每个状态所绑定的依赖越多,依赖追踪在内存上的开销也会变得越来越大,所以从Vue.js2.0开始,引入了虚拟DOM的改变,将细粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件,状态变化后,会通知到组件,组件内部再使用虚拟DOM进行对比,这样可以大量降低状态的依赖数量,从而降低追踪依赖所带来的内存消耗。Vue.js之所以能随意调整粒度,本质上还是要归功于变化侦测,“推”类型的变化侦测可以随意调整粒度。
如何追踪变化
关于object的变化侦测,在JS中如何侦测一个对象发生了变化?有两种方法:通过使用Object.DefineProperty和ES6的Proxy。2.0使用的是Object.DefineProperty,而在3.0中,作者尤雨溪使用ES6中的Proxy重新了这部分代码。但是原理不变。
原理是使用了函数defineReactive来对Object.defineProperty进行了封装。每当从data中的key读取数据的时候,会触发get函数;每当往date的key中设置数据时,会触发set函数。
function defineReactive(data,key,val){ object.defineProperty(date,key,{ enumerable:true, //可枚举 configurable:true, //可配置 get: function(){ return val; }, set: function(){ if(val == newVal){ return; } val = newVal; } }) }
如何收集依赖
在getter中收集依赖,在setter中触发依赖
依赖收集在哪里
假设依赖都保存在window.target中,在defineReactive中新增了一个数组dep,用来存储被收集的依赖。在set被触发时,循环dep来触发收集到的依赖。这样写会存在耦合,所以封装了一个Dep类,其中包含了添加移除更新等等操作来管理依赖,使用这个Dep类可以收集依赖,删除依赖或者向依赖发送通知。
function defineReactive(data,key,val){ let dep = new Dep() //新增 object.defineProperty(date,key,{ enumerable:true, //可枚举 configurable:true, //可配置 get: function(){ dep.depend() return val; }, set: function(){ if(val == newVal){ return; } val = newVal; dep.notify() } }) }
依赖是谁
我们收集的依赖是window.target,收集谁?换句话说,就是当状态发生变化时,通知谁?用到数据的地方有很多,而且类型也不一样,有可能是模板有可能是用户自定义的一个watch,这时需要抽象出一个能集中处理这些情况的一个类,在依赖收集阶段只收集这个封装好的类的实例,通知只通知它一个,它在负责通知其他地方。这个类就是Watcher!
什么是Watcher
Watcher就是一个中介角色,数据发生变化时通知它,它再通知其他地方
vm.$watch('data.a.b',function(){ //做点什么 })
在当data.a.b属性发生变化的时候,会触发第二个参数的函数。
递归侦测所有key
前面介绍的只能侦测数据中的某一个属性,我们希望把数据中的所有属性(包括子属性)都侦测到,所以需要封装一个Observer类,这个类的作用就是把一个数据内的所有属性(包括子属性)都转换为getter/setter的形式,然后去追踪他们的变化,递归遍历子属性调用defineReactive方法。Obsever类用来将正常的object转换为被侦测的object。
关于Object的问题
Vue.js2.0通过Object.defineProperty来将对象的key转换为getter/setter形式来追踪变化,但是它们只能追踪一个数据是否被修改,无法追踪新增属性(object.xxx=xxx)和删除属性(delete this.obj.xxx),所以会产生无法侦测这些属性的变化,所以不会向依赖发送通知,因为在ES6之前,没有提供元编程的能力,无法侦测到一个新属性被添加到了对象中,也无法侦测到一个属性被从对象中删除,为了解决这个问题,Vue.js提供了两个API--vm.delete。
总结
变化侦测就是侦测数据的变化。当数据发生变化时,要能侦测并发出通知。
Object可以通过Object.defineProperty将属性转化成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter。
我们需要在getter中收集有哪些依赖使用了数据。当setter被触发时,去通知getter中收集的依赖数据发生了变化。
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
所谓的依赖,其实就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
Watcher的原理是先把自己设置到全局唯一的指定位置(例如window.target),然后读取数据,因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个Watcher收集到Dep中去。通过这样一个方式,Watcher可以主动去订阅任意一个数据的变化。
此外,还创建了Observer类,它的作用是把一个object中的所有数据(包括子数据)都转换成响应式的,它会侦测object中所有数据包括子数据的变化。
由于ES6之前js没有提供元编程的能力,所以在对象上新增属性和删除属性都是无法被追踪到的
Data通过Observer转换成了getter/setter的形式来追踪变化。
当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
当数据发生了变化时,会触发setter,从而Dep中的依赖(Watcher)发送通知。
Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能会触发用户返回的某个回调函数。
记录一下阅读vue源码的收获