记录《深入浅出Vue.js》这本书中的内容,以笔记的形式了解vue之中涉及的部分原理

Object变化侦测

vue的特性之一就是响应式系统,我们在学习的过程中知道它是通过侦测数据的变化进行视图的更新,而具体到其中的原理是什么呢?

推和拉

变化侦测分为两种类型 一种是推一种是拉。
在Angular和React里面,变化侦测的内容属于”拉“,意思是当状态发生变化的时候,它不知道哪个状态改变了。只知道状态有可能变了。然后发送一个信号告诉框架。
而在Vue当中,变化侦测属于”推”。当状态发送改变的时候,vue立刻就知道了,然后在一定程度上知道哪些状态变了。意味着vue能进行颗粒度更细的更新。

颗粒度

所谓颗粒度呢,就是指某一个状态在发生更新的时候,会影响到多少节点。举个例子来说,假如一个状态绑定着很多个依赖,此时每个依赖表示一个具体的DOM节点,当这个状态改变的时候,会向所有依赖发出通知,进行更新操作。
因此颗粒度越细,开销也就越大,所以vue将粒度调为中等,同时将依赖绑定的DOM节点换为了组件。

如何追踪变化

vue中,先前的版本里,es6对浏览器的支持并不理想,所以采取了defineProperty的方法而非Proxy,通过前者的方法可以很容易的侦测对象的变化,并用getter/setter来对对象进行响应式的处理。

1
2
3
4
5
6
function defineReactive(data,key,val)=>{
Object.defineProperty(data,key,{
enumerable:true,

})
}

如何收集依赖

在本节中,依赖的收集其实是使用到了getter,触发依赖使用到了setter。

依赖收集在哪里?

很清晰的理解,知道要收集依赖,那么势必有保存依赖的地方,而这个地方最好是唯一的,全局性的,因为我们做出的改变需要通知到这个依赖,然后让这样依赖去反馈或者处理我们需要的数据等。
代码的编写方面 在get中push数据到window的全局变量中,然后在set中循环触发依赖。
但是这样编写代码还是有点耦合,于是把收集依赖处理依赖的代码封装成一个Dep类,这个类中有增删改查的方法,还要向依赖发送通知的方法。这样只需要在变化侦测的代码中实例化这个Dep类就可以使用以上方法。

依赖是谁?

在上一小节中,知道依赖最好是存放到一个全局变量中访问和获取,并且在我们调用get的时候负责管理依赖的类会执行修改 set的时候会新增,但我们收集完这些依赖,实际上是要去告诉其他地方或者其他用到依赖的地方,去通知他们要修改,而对于这个起到通知作用的类,我们称之为Watcher。
(在下面会了解到依赖其实就是watcher实例)

Watcher是谁?

在vue的watch方法中,可以知道参数变化之后触发函数的功能。而这个Watcher类也是需要做到这样的事情。在代码中可以看见,作者通过将Watcher类的this赋值给全局变量,然后再读取值触发getter方法,将watcher实例传输到Dep类中,这样Dep就会获取到该实例并且进行增删改查。在增删改查的部分,还会用到watcher的update方法,这就是代码的神奇之处。
写到这里可能有点一头雾水,实际上依赖就是这个watcher实例,这个类就是负责通知Dep是否进行增删改查的。

递归侦测所有key

其实写到这里 已经满足了变化侦测的基本需求了。但是如果我们想检测到数据中所有的属性,光是上面还不够,需要封装一个Observer类。这个类将数据内的全部属性,包括子属性,都转化成getter、setter的形式,然后去追踪它们的变化。

Observer的缺点

上面实现了传入一个obj变成响应式obj的Observer类。但其实它也存在缺点,比如我们要使用点运算符新增一个数据或者delete方法删除一个数据的时候,vue并不能检测到,这是因为getter/setter方法本身只支持数据的修改,不能检测到数据的增加和删除。但是vue提供了两个api来帮助解决这个问题。一个是vm.$set,另外一个是vm.$delete

总结

变化侦测就是侦测数据的变化,当数据发生变化的时候,能侦测到并发出通知。
Object通过defineProperty的方法来将属性转化为响应式来追踪数据的变化。我们需要在getter中收集依赖,当setter被触发的时候通知getter中收集的依赖数据发生变化。
收集依赖需要为依赖找到一个存储依赖的地方,为此创建了Dep用于收集依赖,删除依赖和向依赖发送信息。
所谓的依赖,其实就是Watcher。只有Watcher触发的getter才会收集依赖,哪个watcher触发了getter,就把哪个watcher收集到Dep。当数据发生变化的时候,会循环依赖列表,把所有的watcher都通知一遍。

Array变化侦测

上文介绍了Object的变化侦测 但是getter和setter方法并不会触发数组的方法,比如push,pull等等,下面讲解vue是如何操作使得数组的操作能够被检测

如何追踪变化

我们知道Object的变化是靠setter来追踪的,一旦数据发送了变化 就触发setter。
那么我们只要操作数组的时候,通知到一个容器让他存储这种变化即可。
可惜的是在es6之前没有提供元编程的能力,也没有提供拦截原型方法的能力,但是程序员可以自己创建一个方法,取拦截Array.prototype之后我们想使用Array上面的方法去操作数组的时候,使用的都是拦截器上面的方法,然后在拦截器中使用原生Array原型上面的方法去操作数组。

拦截器

拦截器其实是一个和Array.prototype一样的Object 里面包含的属性一模一样,只不过这个object中某些可以改变数组和自身内容的方法是处理过的。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method)){
//缓存原始方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods,methods,{
value:function mutator(...args){
return original.apply(this,args);//触发Array上面的方法
},
enumerable:false,
writable:true,
configurable:true
})
}

在上面的代码中 创建了变量arrayMethods,继承自Array.prototype,所以有它的全部功能。然后用arrayMethods去覆盖数组原型。
接着封装数组的方法在Object.defineProperty
假如使用的是push,那么实际上调用的是arrayMethods.push然后arrayMethods.push是函数mutator所以实际执行的是mutator函数
最后在mutator中执行original(它是原生数组原型上面的方法 比如push)来做它应该做的事情。
因此我们就可以在mutator中做一些其他的事情 比如发送变化通知。