vue3中ref和reactive原理

123 阅读5分钟

0.前置知识

1. Proxy

const proxyObj = new Proxy(object, handler)
// 其中new Proxy常见一个新的proxy对象,将原来对obejct的访问(object.name 或者object.age = 11)都改为对 proxyObj的访问,即object.age = 11
var target = {
  a:1,
  b:{
    c:2,
    d:{e:3}
  },
  f: {z: 3}
}
var handler = {
  get:function(target, prop, receiver){
    var val = Reflect.get(target,prop)
    console.log('触发get:',prop)
    if(val !== null && typeof val==='object') {
        return new Proxy(val,handler) // 在触发第一层代理的时候,然后代理内层属如果为对象,再将代理内层属性作为target继续往下代理
    }
    // 代理完毕后再拿一次
    return Reflect.get(target,prop)
  },
  set:function(target,key,value,receiver){
    console.log('触发set:',key,value)
    return Reflect.set(target,key,value,receiver)
  }
}
var proxy = new Proxy(target,handler)
 
proxy.b.d.e = 4 
// 输出  触发get b,get d, get e

proxy本身就是一个对象的代理,而proxy自己本身也是一个代理对象,proxy对象可以理解为:对原始对象与操作之间进行一层拦截,这样只要对proxyObj操作就相当于通过代理对象来操作targetObj,可以拦截的操作如下所示:
image.png 所以ES6引入了Proxy代理

www.cnblogs.com/tugenhua070…
2.Reflect
Reflect是ES6为了操作对象而新增的API, 为什么要添加Reflect对象呢?它这样设计的目的是为了什么? 我们可能会觉得reflect对象设计的有点多此一举,我明明可以直接获取对象的属性或者设置对象的属性,为什么还要通过reflect对象来对obj的数据进行获取或者设置呢?
例如:relfect.set(obj, 'age', 29)
具体原因有以下两点:
(1)数据和逻辑分离:因为es6希望我们的数据和代码的逻辑分离,那么我们可以很好的将代码的逻辑都集中在reflect.set对象中,这样就可以通过reflect.get/set 来替代obj属性的设置或者获取(通过reflect.set(obj, 'age', 11)可以更改obj对象的age属性,在obj.age属性值是会收到更改的,本质上是一样的)
image.png

(2)错误处理:在使用对象的 Object.defineProperty(obj, name, {})时,如果出现异常的话,会抛出一个错误,需要使用try catch去捕获,但是使用 Reflect.defineProperty(obj, name, desc) 则会返回false。 www.pudn.com/news/627b2d…

0.前置知识

(1) 背景

我们都知道在vue3中对vue2的响应式原理中对于数据的监听,由原来的defineproperty改为了proxy。之所以这么改,肯定是因为 defineproperty 自身存在的缺陷。
因为defineproperty在设计之初并不是为了达到监听数据的目的,但是由于后面使用这种方法对数据进行监听的场景越来越多,但是缺陷却越来越明显:只能监听对象属性的改变,不能监听属性的删除和新增(这也就导致了对于数组的增加和删除是监听不到的)
defineProperty不能监听数组变化的本质是:defineproperty只能监听对象已有属性的更新,不能监听属性的新增和删除。 通常有人总结几点,只能监听对象不能监听数组,这一点其实数组是可以监听到的只是不能监听到数组的删减,还有人说不能监听到对象的删减,其实这是和上一点重合的,因此可以合并为一点就是不能监听到对象属性的增加和删除,具体表现为两处:对象属性的删除和增加无法监听,数组的push方法和删除等方法无法监听) 小结:因为不能监听数据属性的增加和删除,所以不能监听数组的增加。
(2) proxy对象

const proxyObj = new Proxy(object, handler)
// 其中new Proxy常见一个新的proxy对象,将原来对obejct的访问(object.name 或者object.age = 11)都改为对 proxyObj的访问,即object.age = 11

proxy对象可以理解为:对原始对象与操作之间进行一层拦截,这样只要对proxyObj操作就相当于通过代理对象来操作targetObj,可以拦截的操作如下所示:
image.png 所以ES6引入了Proxy代理

var target = {
  a:1,
  b:{
    c:2,
    d:{e:3}
  },
  f: {z: 3}
}
var handler = {
  get:function(target, prop, receiver){
    var val = Reflect.get(target,prop)
    console.log('触发get:',prop)
    if(val !== null && typeof val==='object') {
        return new Proxy(val,handler) // 在触发第一层代理的时候,然后代理内层属如果为对象,再将代理内层属性作为target继续往下代理
    }
    // 代理完毕后再拿一次
    return Reflect.get(target,prop)
  },
  set:function(target,key,value,receiver){
    console.log('触发set:',key,value)
    return Reflect.set(target,key,value,receiver)
  }
}
var proxy = new Proxy(target,handler)
 
proxy.b.d.e = 4 
// 输出  触发get b,get d, get e

此外,出于性能考虑:
性能问题1: 如果对象层级嵌套过深,那么就会导致使用defineProperty性能的开销加大。 因为defineProperty一般是在组件初始化的时候就要通过递归完“监听”。但是通过proxy代理,可以在调用的时候再去判断目标属性是否为引用值,然后在进行递归proxy代理。

注意:所以proxy如果想实现深度监听,也需要实现一个类似上文的Observer的递归函数 性能问题2: vue2中如果对数组中每个index值都进行defineProperty监听的话会增加性能的消耗,因此vue2中重写了数组原型的方法push\pop来出发响应式。

综上所诉:vue3中使用proxy代替defineProperty来实现数据响应式,主要原因如下: 1.bug修复:修复对象和数组中的增加属性无法监听的问题 2.性能考虑:defineproperty需要在初始化的时候对数据的每个属性进行递归绑定,但是proxy只需要对对象整体进行代理即可实现拦截,此外对于深层的数据是在调用时进行递归的。

2.ref

3.reactive

在vue3中,创建响应式对象的方法有reactive, shallowReactive, readonly, shallowReadonly,他们内部都是调用createReactiveObject创建一个proxy对象,只是传递给proxy代理对象的handler不同而已。主要有两类区别,shallow与否和readonly与否,shallow用来区分当触发get的时候,如果返回值是个对象的时候,是否继续把他转换为相应的代理对象,readonly正如字面所示,用于控制是否可以通过set或者delete去修改对应的target对象。

参考文献:zhuanlan.zhihu.com/p/283053564