深入浅出vue.js
记录《深入浅出Vue.js》这本书中的内容,以笔记的形式了解vue之中涉及的部分原理
Object变化侦测
vue的特性之一就是响应式系统,我们在学习的过程中知道它是通过侦测数据的变化进行视图的更新,而具体到其中的原理是什么呢?
推和拉
变化侦测分为两种类型 一种是推一种是拉。
在Angular和React里面,变化侦测的内容属于”拉“,意思是当状态发生变化的时候,它不知道哪个状态改变了。只知道状态有可能变了。然后发送一个信号告诉框架。
而在Vue当中,变化侦测属于”推”。当状态发送改变的时候,vue立刻就知道了,然后在一定程度上知道哪些状态变了。意味着vue能进行颗粒度更细的更新。
颗粒度
所谓颗粒度呢,就是指某一个状态在发生更新的时候,会影响到多少节点。举个例子来说,假如一个状态绑定着很多个依赖,此时每个依赖表示一个具体的DOM节点,当这个状态改变的时候,会向所有依赖发出通知,进行更新操作。
因此颗粒度越细,开销也就越大,所以vue将粒度调为中等,同时将依赖绑定的DOM节点换为了组件。
如何追踪变化
vue中,先前的版本里,es6对浏览器的支持并不理想,所以采取了defineProperty
的方法而非Proxy
,通过前者的方法可以很容易的侦测对象的变化,并用getter/setter来对对象进行响应式的处理。1
2
3
4
5
6function 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
14const 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
中做一些其他的事情 比如发送变化通知。