前端碎碎念之vue双向绑定
本文章会详细介绍vue2和vue3的双向绑定
文章目录
一、如何实现vue2的双向绑定
1.原理
其实vue2的双向绑定很简单,还记得ES5里面的Object.defineProperty( )这个方法吗?可以用这个方法来监听一个对象的属性,比如是否可以删除,是否可以枚举等,emmm跑题了,这里我们要说下setter和getter
var book = {
_year : 2004,
edition : 1
};
Object.defineProperty(book,"year",{
get : function () {
alert(this._year);
},
set : function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year; // 结果:2004
book.year = 2005; //设置的属性要和defineProperty()函数里面的属性相同
console.log(book.edition); // 结果:2
这里我们定义了一个对象book,_year是一个访问器属性,当我们访问year时,会触发get函数,在修改时,会触发set函数
好了,基础用法知道了,我们看看vue2中怎么用这个方法吧
在vue2中,我们在data中定义了数据,在视图层用小胡子语法绑定时,会触发data对象中属性的get方法,在get方法中会触发addsub(),进行添加一个watcher,用来监听数据
在修改数据的时候,会触发set方法,在set方法中会首先对比前一个数据,如果有变化,就通知watcher自动触发render当前组件,生成新的虚拟dom树,然后记录差别,最后加载操作,局部修改到真实的dom树上面
2.vue2的双向数据绑定缺点:
1.无法监听对象内属性的增加和删除
2.无法监听数组变化,当直接使用索引(index)设置数组项时,不会被vue检测到
3.如果一个属性的层级太深,则会在引起性能的问题
3.解决方案:
1.对于缺点一,vue2提供了Vue.set(obj, key, value)和vue.delete(obj,key)
2.对于缺点二,vue重写了一些数组方法是,具体操作是首先执行原生的数据方法,对于push等方法通过switch选择后,进入不同case将数据变成响应式
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//缓存原生方法,之后调用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
},
})
})
function protoAugment (target,src) {
target.__proto__ = src
}
// 调用
let obarr = []
protoAugment(obarr, arrayMethods)
经过以上的代码可以看出,只会更改我们给定数组(obarr)的相关方法,而不会污染Array的原生方法,因此其他普通数组不受影响。对于不支持__proto__属性的浏览器,直接使用Object.defineProperty从新定义相关属性。Vue的实现方法正如上,更改我们需要监听的Array数组属性值(属性值为函数),在监听函数里执行数组的原生方法,并通知所有注册的观察者进行响应式处理。
二、vue3数据双向绑定
1.原理
vue3双向绑定的实现是基于es6的proxy生成的,基于proxy生成一个代理对象,在创建vue实例时,会返回一个proxy代理
proxy如何实现响应式的数据响应绑定呢?
在reactive函数中,会通过传入一个target来实现响应,在vue定义时的data会作为target传入,这个函数返回一个代理,通过代理修改值时会触发trigger更新,同时reactive如果发现target是对象,会进行拦截,如果是就会通过newproxy( target,handler),当操作代理对象时会触发handler方法handler中可放一个set(),get(),deleteproprty()当获取属性是,触发get(),修改属性时触发set,删除时触发delete()在get中收集依赖,set中会触发依赖,如果target是一个对象的话,会递归调用reactive去产生代理,同时为了防止重复代理,其实还创建了一个哈希表,在reactive中,会查看哈希表,如果存在,直接return,无需再次代理,如果没有,就加入
2.优点
vue中监听的是属性,而vue3中监听的对象,因此可以解决缺点一和二,同时在数据层级太深是vue3是需要时才会进行递归,不会太损耗性能
三、如何实现依赖收集(vue2为例)
观察者模式是一种实现一对多关系解耦的行为设计模式。它主要涉及两个角色:观察目标、观察者。Vue源码中实现依赖收集,实现了三个类: - Dep:扮演观察目标的角色,每一个数据都会有Dep类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观察者,当本数据变更时,调用dep.notify()通知观察者 - Watcher:扮演观察者的角色,进行观察者函数的包装处理。如render()函数,会被进行包装成一个Watcher实例 - Observer:辅助的可观测类,数组/对象通过它的转化,可成为可观测数据
1.dep
Dep类实例依附于每个数据而出来,用来管理依赖数据的Watcher类实例,在取或改某一数据时,可访问dep.target,在后面的依赖收集部分中getter中会调用dep.depend()而setter会调用dep.notify()
假如有一组数据:
{
a: 1,
b: [2, 3, 4],
c: {
d: 5
}
}
添加完数据观测会变成
{
__ob__, // Observer类的实例
a: 1,
b: [2, 3, 4],
c: {
__ob__, // Observer类的实例
d: 5
}
}
我们发现,对象有了_ob_,其实_ob_上的Observer是将对象变成了响应式, 将Observer类的实例挂载在__ob__属性上,提供后续观测数据使用,以及避免被重复实例化。然后,实例化Dep类实例,并且将对象/数组作为value属性保存下来 ,通过判断是对象还是数组,对象执行walk()把每一项属性都变为可观测数据,数组就执observeArray()过程,递归地对数组元素调用observe(),处理元素还是数组的情况
2.watcher
Watcher扮演的角色是观察者,它关心数据,在数据变化后能够获得通知,并作出处理。一个组件里可以有多个Watcher类实例
在Watcher类里做的事情,概括起来则是:
1、 deps:缓存上一轮执行观察者函数用到的dep实例 - depIds:Hash表,用于快速查找 - newDeps:存储本轮执行观察者函数用到的dep实例 - newDepIds:Hash表,用于快速查找
2、进行初始求值watcher.get()方法
3、在初始准备工作里,会将当前Watcher实例赋给Dep.target,清空数组newDeps、newDepIds,Dep.target指向dep
4、触发数据的getter,从而执行watcher.addDep()方法,将特定的数据记为依赖
5、对每个数据执行watcher.addDep(dep)后,数据对应的dep如果在newDeps里不存在,就会加入到newDeps里,保证同样的依赖只能收集一次。并且如果在deps不存在,表示上一轮计算中,当前watcher未依赖过某个数据,
6、首先释放Dep.target,然后拿newDeps和deps进行对比,接着进行以下的处理: 如果newDeps里不存在,deps里存在的数据,则是过期的缓存数据。dep.subs移除掉当前watcher - 将newDeps赋给deps,表示缓存本轮的计算结果
8、进行了set时,对dep.subs这一观察者队列里的watchers进行通知,触发dep.notify(),从而执行watcher.update()方法,update()会重复以上