什么是变化侦测
渲染:
vue.js会自动通过状态生成DOM,并将其输出到页面上显示出来
在运行时应用内部状态不断变化,此时需要不断渲染。那我们如何确定发生了哪些变化?
这个时候就需要变化侦测了。
变化侦测
- 拉:Angular、React,当状态发生变化时,它只知道状态变了,给框架发送信息,而框架内部则进行暴力对比找出需要渲染的DOM节点,在Angular属于脏检查,在React使用的是虚拟DOM。
- 推:Vue.js,当状态发生变化,vue立刻就知道了,而且在一定程度指导的消息更多,可以进行更细粒度的更新。
更细粒度的更新:假如有一个状态绑定了好多个依赖,每个依赖表示一个具体的DOM节点,,当状态发生变化,向依赖发送通知,进行DOM更新,相较而言,拉的粒度是最粗的。当然粒度越细,每个状态绑定的依赖就越多,依赖追踪在内存中的开销就越大,因此从Vue2.0开始就调整依赖绑定的粒度为组件。通知到组件之后,组件内部再用虚拟DOM进行对比。这样可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。
如何追踪变化
追踪变化有两个方法:Object.defineProperty和ES6的Proxy
根据Object.defineProperty可以侦测到对象的变化,可以写出如下代码
function(data,key,val){
//参数123,1:定义属性的对象,2:定义或修改的属性的名称
//,3.定义或修改的属性描述符
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
return val
},
set:function(newVal){
if(val===newVal){
return
}
val=newVal
}
})
}
函数defineReactive用来对Object.defineProperty进行封装。
作用:定义一个响应式数据,也就是在这个函数中进行变化跟踪,封装后只需要传递data,key,val就行了。
封装好之后每当从data的key中读取数据时,get属性杯触发;每当往data的key中设置数据时set函数被触发
如何收集依赖
收集依赖就是把用到数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。
就是
在getter中收集,在setter中触发依赖
依赖收集在哪里
依赖收集代码封装成一个Dep类,专门管理依赖。可以收集依赖、删除依赖或向依赖发送通知等。
dep类
function defineReactive(data,key,val){
let dep=new dep()//修改
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend()//修改
return val
},
set:function(newVal){
if(val===newVal){
return
}
val=newVal
dep.notify()//新增
}
})
}
export default class Dep{
constructor(){
this.subs=[]
}
addSub(sub){
this.subs.push(sub)
}
removeSub(sub)(
remove(this.subs,sub)
)
depend(){
if(window.target)//target:节点
{
this.addSub(window.target)
}
}
notify(){
//选取数组的一部分,并返回一个新数组。
const subs=this.subs.slice()
for(let i=0;i<subs.length;i++){
subs[i].updata()
}
}
function remove(arr,item){
if(arr.length){
const index =arr.indexOf(item)
if(index>-1){
//从数组中添加或删除元素。删除从索引inidex后的一个元素
return arr.splice(index,1)
}
}
}
}
defineReactive
function defineReactive(data,key,val){
let dep=new dep()//修改
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend()//修改 添加依赖
return val
},
set:function(newVal){
if(val===newVal){
return
}
val=newVal
dep.notify()//新增,通知依赖
}
})
}
依赖是谁
在上面的代码里window.target是我们的依赖,那么它又是什么呢?
我们使用数据的地方很多,类型也不太一样,可能是模板,也可能是用户写的watch,我们可以抽象出一个处理这些情况的类叫watcher
所以我们要收集的就是watcher
什么是watcher
Watcher是一个中介角色,数据发生变化时通知它,然后它再通知其他地方。
vm.$watch('a.b.c'),function(newVal,oldVal){
}
这段代码表示当data.a.b.c发生变化是,触发第二个参数中的函数
怎么去实现这个功能呢?是不是和上面讲的把watcher实例添加到data.a.b.c属性的dep中就可以了。这样子,当data.a.b.c属性的值发生变化时,通知watcher,接着watcher再执行回调函数。
export default class Watcher{
constructor(vm,expOrFn,cb){
this.vm=vm
//执行this.getter(),就饿可以读取data.a.b.c的内容
this.getter=ParsePath(expOrFn)
this.cb=cb
this.value=value
}
get(){
window.target=this
let value=this.getter.call(this.vm,this.vm)
window.target=undefined
return value
}
update(){
const oldVlaue=this.value
this.value=this.get()
this.cb.call(this.vm,this.value,oldValue)
}
}
这段代码作用:把自己主动添加到data.a.b.c的Dep中
在get方法中把window.target设置成了this,也就是当前watcher实例,然后再读一下data.a.b.c的值,这会触发getter,也就触发收集依赖的逻辑。从window.target中读取一个依赖并添加到dep中
也就是说只要现在window.target赋一个this,然后再读一下值,触发getter就可以把this主动添加到keypath的Dep中。
依赖注入到Dep中后、每当data.a.b.c的值发生变化,就会让依赖列表中的所有依赖循环调用update方法,也就是watcher中的updata方法。而update方法会执行参数中的回调函数,将value和oldValue传到参数中
递归侦测所有key
前面已经实现了侦测数据中某一个属性,现在希望把数据中的所有属性都侦测到,所以封装一个
Observer:这个类的作用是将一个数据内的所有属性(包括子属性)都转换成getter/setter的形式,然后去追踪他们的变化:
export class Observer{
constructor (value){
this.value=value
if(!Array.isArray(value)){
this.walk(value)
}
}
/*walk会将每一个属性都转换成getter/setter的形式来侦测变化
*这个方法只有在数据类型为object时被调用
*
*/
walk(obj){
const keys=Object.keys(obj)
for(let i=0 ; i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
function defineReactive(data,key,val){
//新增递归子属性
if(typeof val === 'object')
{
new Observer(val)
}
let dep=new dep()
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.depend()
return val
},
set:function(newVal){
if(val===newVal){
return
}
val=newVal
dep.notify()
}
})
}
}
Observer类:用来将一个正常的object转换成被侦测的object
只有object类的数据才会调用walk将每一个属性转换成getter/setter的形式来侦测变化
在defineReactive中新增 new Observer(val)来递归子属性,这样就可以data中的所有属性都转换成getter/setter形式来侦测变化
只要我们将一个object传到observer中,那么这个object就会变成响应式的object.
object的问题
因为是通过setter和getter来侦测变化,所以如果是新增属性则无法侦测,删除也是
为了解决这个问题vue.js引入了两个API——vm.$set与vm.delete
引用申明
本博文为深入浅出vue.js的学习笔记,若侵则删。