我们都知道,在Vue2中的数据响应式原理存在许多缺陷。
例如无法对新增和直接删除的数据做到响应式,无法直接操作数组进行响应式处理等等。
而在Vue3中,作者很好的解决了这些缺陷,让我们来对比一下Vue2与Vue3对数据响应式处理的具体区别吧
Vue2数据响应式原理
vue2中的响应式是通过object.defineProperty
遍历每一个属性(对于深层嵌套的对象则会进行递归处理)并为其添加上getter
和setter
方法用来来监听数据的读取与改变,也就是我们所说的数据劫持,然后结合观察者模式,也就是==发布者-订阅者模式(Dep-Watcher)==通知页面视图发生改变,从而实现数据的响应式原理,数据劫持的简单实现过程如下所示:
const p = document.getElementsByTagName('p')[0];
let data={
name:'张三',
age:19
}
const obs = new obServer(data);
console.log(obs);
function obServer(obj){
let keys = Object.keys(obj);//获取对象的所有属性
keys.forEach(k=>{//循环遍历每一个属性
Object.defineProperty(this,k,{//为每一个属性进行劫持操作
get(){
return obj[k];
},
set(val){
console.log('数据改变啦');
obj[k]=val
}
})
})
}
1.页面是如何更新的?
我们得了解一下什么是监听器,订阅器和订阅者。
- 监听器
Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者; - 订阅器
Dep
,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理; - 订阅者
Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;
首先在我们为每个监听属性添加上getter
和setter
方法进行收集依赖和触发依赖,然后通过创建一个Dep
实例对监听器Observer
和订阅者Watcher
进行统一管理。当属性被发生更改时,调用setter
方法进行派发更新,通知订阅者Watcher
告诉它进行页面更新。
2.Vue2响应式存在的缺陷
一、新增加的属性和删除属性没有响应式
由于该版本的Vue会在初始化实例时就对data里面的属性进行getter/setter 转化,且由于只有get()、set() 方式,所以只能捕获到属性读取和修改操作,当 新增、删除属性时,捕获不到,导致界面也不会更新。
处理办法:
在vue2中引入了两个API用于对新增属性,删除属性的响应式处理,分别是$set
与$delete
,详细操作如下代码所示:
<script>
export default {
data() {
return {
obj: {
name: '张三',
age: 16
}
}
},
methods: {
// 添加属性
// 当我们这样新增属性时,界面是不会有变化的
addSex1() {
this.obj.sex = '男'
}
// 我们要这么去添加属性,页面才会有变化
addSex2(){
this.$set(this.obj, 'sex', '男')
}
//同理,删除属性,当我们这样删除属性时,界面是不会有变化的
deleteName1() {
delete this.obj.name
}
// 我们要这么去删除属性,页面才会有变化
deleteName2(){
this.$delete(this.obj, 'name')
}
}
}
</script>
二、数组的部分操作没有响应式
在Vue2当中,是没有办法直接操作数组下标来实现数组的响应式的,之所以这样是因为直接对数组进行property操作代价太高,一般来说我们对数组的操作都是增加删除,参考对象的增加删除可以看到代价太大,且一般数组的键值对数量比较庞大,这也导致了直接操作数组下标付出的性能代价太高,所以作者是通过重写了7个常用的数组方法,我们需要通过调用这些方法来对数组的响应式进行实现,具体的数组方法如下所示:
- array.pop()
- array.push()
- array.shift()
- array.unshift()
- array.sort()
- arry.reverse()
- array.splice()
只有通过以上的7种方法对数组进行增删改才能实现数据的响应式,若是直接通过下标来修改,则没有响应式,如下代码所示
fn1() {
this.arr[0] = 100
}
解决办法
fn1 () {
// 方法1: splice方法
this.arr.splice(0,1,100)
},
fn2 () {
// 方法2: $set
this.$set(this.arr, "0", 100)
}
三、对于深层嵌套的对象属性有着性能上的问题。
由于Vue2是通过object.defineProperty
来对数据进行响应式处理,当我们需要监测深层对象属性时,我们需要通过层层递归的操作遍历所有属性对其进行设置响应式,这也导致了每次进行数据响应式处理将会花费很高的代价。因此,Vue3的通过proxy很好的解决了这一问题。
Vue3数据响应式原理
在Vue3当中,对于基本数据类型,还是依赖于了Vue2中的object.defineProperty
操作,而对于对象和数组操作,Vue3采用了es6的proxy结合reflect对数据拦截进行响应式处理。
通过Proxy(代理): 拦截对象中任意属性的变化,包括:属性值的读写,属性的增加,属性的删除等。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等,这是Object.defineProperty不具备的
通过Reflect(反射): 对源对象的属性进行操作proxy是es6版本出现的一种对对象的操作方式。
Vue3对象数组响应式的基本实现思路:通过 new Proxy 代理了 obj 对象,然后通过 get、set、deleteProperty 函数代理了对象的读取、修改和删除操作,从而实现了响应式的功能。
const p = new proxy(person, {
// 有人读取p 身上的某个属性
get(target, propName) {
console.log(`有人读取了p身上的${propName}属性`, 更新界面);
return Reflect.get(target, propName) //return 出去一个返回值
},
// 给p对象身上增加/ 修改某个属性时调用
set(target, propName, value) {
console.log(`有人修改/增加了p身上的${propName}属性`, 更新界面);
Reflect.set(target, propName, value)
},
deleteproperty(target, propName) {
console.log(`有人删除了p身上的${propName}属性`, 更新界面);
return Reflect.deleteProperty(target, value) // 删除一个属性
}
})
Vue3只有在getter时才对对象的下一层进行劫持,真正访问到的内部对象才会变成响应式,大大优化了性能。