Vue3中的reactive与readonly
前言
本文主要整理关于在
vue3中所提及到的所有与reactive以及readonly相关的API,通过对比每个API的作用以及使用场景,新增对vue3中相关的API的认知,主要借助于vue3官方API的阅读!
ES2015的Proxy与Reflect
在开始学习关于
vue3的reactive之前,先来了解一下关于什么是Proxy以及Reflect,Proxy与Reflect都是ES6中引入的新特性,它们通常一起使用以提供更灵活和强大的对象操作能力!
Proxy
Proxy用于修改对象某些操作的默认行为,等同于在语言层面对这个默认行为做出的修改,属于一种“元编程(编程的编程)”,可以理解为在目标对象做出默认响应(如属性查找、赋值、枚举、函数调用等等)之前提供一到“拦截”动作,外界对该对象默认行为的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy原意是代理,用于表示用它来“代理”某些操作,可以简单理解为“代理器”,语法形式如下:
| 1 | const p = new Proxy(target, handler) | 
🤩 参数说明:
- target: 要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数,甚至另外一个代理);
- handler: 一个普通对象,其成员一般都是函数类型的,函数分别定义了在执行对象的默认操作时,将提供的对应的拦截动作;
关于在handler对象中所提供的相关属性可以有 👇 的成员:
- getPrototypeOf: Object.getProtptypeOf方法的拦截器;
- setPrototypeOf: Object.setPrototypeOf方法的拦截器;
- isExtensible: Object.isExtensible方法的拦截器;
- preventExtensions: Object.preventExtensions方法的拦截器;
- getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor方法的拦截器;
- defineProperty: Object.defineProperty方法的拦截器;
- has: in操作符的拦截器;
- get: 对象属性读取操作的拦截器;
- set: 对象属性赋值操作的拦截器;
- deleteProperty: delete操作符的拦截器;
- ownKeys: Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols的拦截器;
- apply: 函数调用操作的拦截器
- construct: new操作符的拦截器;
比如有 👇 的一个例子:
| 1 | const handler = { | 
👆 上述例子中handler重新定义了对象的get动作,当从一个对象中获取属性时,如果属性存在则返回对应的值,不存在则返回默认的一个37数字。然后在p2中没有定义任何拦截操作,则说明将不做任何的逻辑处理!
😕 关于这个Proxy一般都有哪些应用场景呢? 👉 可以作为对象的验证器(通过重写set方法)、扩展构造函数(通过重写construct以及apply)等等!
Reflect
Reflect是一个内置的对象,它提供了拦截Javascript操作的方法,这些方法与上述的proxy_handler的方法一样,**Reflect不是一个函数对象**,因此它不可构造!Reflect的所有属性和方法都是静态的(就像Math一样)
以下是关于Reflect的一个简单运用:
| 1 | const obj = { | 
使用Reflext可以让代码更加具有可读性和可维护性,因为它提供了一种统一的方式来调用基本操作,并且在操作失败时提供了更加友好的错误处理方式
reactive成员一览
vue3中的相关reactive成员的实现,则都是基于上述的Proxy以及Reflect来实现的!
对象响应式核心reactive()
reactive()接收一个对象参数,返回这个对象的响应式代理,通过该API所创建出来的对象是一个Proxy对象,一般只操作这个Proxy对象,而不会去使用原始对象。
以下是对应的一个简单例子:
| 1 | const obj = reactive({ count: 0 }) | 
😕 而这个过程发生了什么事呢,下面来简单分析一波关于这个API的实现(隐藏非关联的代码):
| 1 | // packages/reactivity/src/reactive.ts | 
🌟 通过开篇关于Proxy的使用我们可以得知,上述代码中的handler,必须是一个普通的对象,其所提供的属性对应方法将拦截了target对象的默认操作!
  在vue3中,凭借typescript,在其公共基础类型库中,通过自定义ProxyHandler<T extends object>接口,将handler中所提供的相关属性都罗列出来,为方便编写handler提供编写说明与提示:
| 1 | interface ProxyHandler<T extends object> { | 
🌟 上述通过结合BaseReactiveHandler以及MutableReactiveHandler,最终实现对get与set方法的重写,来拦截相关的属性存取操作,通过在get获取属性时对该key设置监听,然后在set赋值属性时,触发监听动作,实现对对象属性的默认监听与拦截操作(这里是将触发对应的界面自动更新)!
😕 那么通过这个reactive()方法所创建出来的Proxy对象与其他对象有什么区别吗?
👆 从上面我们可以看出所创建出来的Proxy对象拥有两个属性:target + handler,这与我们开头所习得的知识一样,然后从handler的继承关系来看,与源码的执行结果是一致的,继承关系如下图所示:
🤔 上述关于Target类型是什么呢?为什么要设计这样子的一个类型?
👉 首先从上述的代码我们发现关于这个Target就是一普通的对象,只不过它可能拥有着__v_skip、__v_isReactive、__v_isReadonly、__v_isShallow、__v_raw这几个属性中的一个或者多个属性,在上述的createReactiveObject方法中,我们用一句话:对象已经被代理了,来判断对象是否已被代理过,其对应的实现如下:
| 1 | if(target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])){ | 
🌟 也就是说,通过reactive()所创建出来的代理对象,可通过ReactiveFlags.RAW以及ReactiveFlags.IS_REACTIVE来访问其值!
😕 这里发现我们所传递的对象中是没有上面的这些属性的,但是通过Proxy,我们发现在其handler的get属性对应的方法中声明了对这些属性的访问:
| 1 | class BaseReactiveHandler implements ProxyHandler<Target>{ | 
🌟 这里自定义了对__v_isReactive、__v_isReadonly、__v_isShallow、__v_raw这几个属性的访问,然后根据当前对象所采用的模式(通过reactive()、readonly()、shallowReactive()三者之一),来提供对应的get以及set操作!而且,还在对应的handler中保留了通过__v_raw的方式来访问原始对象(但这种方式尽量少用,而是使用toRaw),关于这个toRaw的方法定义如下:
| 1 | export function toRaw<T>(observed: T):T { | 
🥹 与toRaw相关的另外一个API是makeRaw,关于此方法的定义如下
| 1 | export type Raw<T> = T & { [RawSymbol]?: true } | 
 🌟 从方法中我们可以看出此方法通过接收一对象,然后将其转化成为不可响应式的对象, 🤔 这里为什么添加了这个ReactiveFlags.SKIP就可以认为它是一个不可响应的对象呢?
 👉 我们在上述的代码中,利用伪代码对象非合法的类型,来代替对一个对象是否可代理的检测,关于此伪代码的实现如下:
 
| 1 | function targetTypeMap(rawType: string) { | 
🤩 通过调用
getTargetType(value)获取到对象的类型,如果是TargetType.INVALID的话,则说明是不可被响应式的对象!
shallowReactive()
reactive()的浅层实现,一般比较少用,因为它创建的树具有不一致的响应行为,可能很难理解与调试
isShallow()
检查传入的对象是否是一个
shallow对象,该方法定义如下:
| 1 | export function isShallow(value: unknown): boolean { | 
isReactive()
检查一个对象是否由
reactive()或者shallowReactive()创建出来的代理对象,该方法定义如下:
| 1 | export function isReactive(value: unknown): boolean { | 
isProxy()
检查一个对象是否由
reactive()方法所创建出来的Proxy对象,该方法定义如下:
| 1 | export function isProxy(value: any): boolean{ | 
readonly成员一览
readonly()
接收一个对象或者一个ref,返回一个原value的只读代理,只读代理是深层的,对任何嵌套属性的访问都将是只读的!
关于此方法的实现如下:
| 1 | export function readonly<T extends object>( | 
🌟 readonly()的实现与reactive()的实现类似,就是传递的参数不一样而已,主要在于这个readonlyHandlers的实现不同,如下所示:
| 1 | class ReadonlyReactiveHandler extends BaseReactiveHandler { | 
🌟 从上面的handler我们可以发现,readonly()仅仅是实现了get方法,然后对set以及deleteProperty进行了直接返回,也就是不允许对对象的属性新型编辑操作!
isReadonly()
检查传入的值是否为只读对象,只读对象的属性可以更改,但他们不能通过传入的对象来直接赋值,这有点类似于
const obj,而通过readonly()以及shallowReadonly()创建的代理都是只读的,因为它们在对应的代理对象的handler中将set方法给铲掉了!关于isReadonly()方法定义如下:
| 1 | export function isReadonly(value: unknown): boolean { | 
🌟 通过访问一个对象中的属性__v_is_readOnly值是否为true来判断是否为一个readonly对象
shallowReadonly()
readonly()的浅层作用形式
总结
关于上述对
reactive()以及其关联的其他API进行一个分析之后,发现当我们通过const state = reactive({count: 1})这个语句执行时,往被代理的对象中新增一系列自定义属性(__v_isXXX),代表正常响应式的、还是只读响应式的、还是浅层响应式的