前言

本文主要整理关于在vue3中所提及到的所有与reactive以及readonly相关的API,通过对比每个API的作用以及使用场景,新增对vue3中相关的API的认知,主要借助于vue3官方API的阅读!

ES2015的Proxy与Reflect

在开始学习关于vue3reactive之前,先来了解一下关于什么是Proxy以及ReflectProxyReflect都是ES6中引入的新特性,它们通常一起使用以提供更灵活和强大的对象操作能力!

Proxy

Proxy用于修改对象某些操作的默认行为,等同于在语言层面对这个默认行为做出的修改,属于一种“元编程(编程的编程)”,可以理解为在目标对象做出默认响应(如属性查找、赋值、枚举、函数调用等等)之前提供一到“拦截”动作,外界对该对象默认行为的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy原意是代理,用于表示用它来“代理”某些操作,可以简单理解为“代理器”,语法形式如下:

1
const p = new Proxy(target, handler)

🤩 参数说明:

  1. target: 要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数,甚至另外一个代理);
  2. handler: 一个普通对象,其成员一般都是函数类型的,函数分别定义了在执行对象的默认操作时,将提供的对应的拦截动作;

关于在handler对象中所提供的相关属性可以有 👇 的成员:

  1. getPrototypeOf: Object.getProtptypeOf方法的拦截器;
  2. setPrototypeOf: Object.setPrototypeOf方法的拦截器;
  3. isExtensible: Object.isExtensible方法的拦截器;
  4. preventExtensions: Object.preventExtensions方法的拦截器;
  5. getOwnPropertyDescriptor: Object.getOwnPropertyDescriptor方法的拦截器;
  6. defineProperty: Object.defineProperty方法的拦截器;
  7. has: in操作符的拦截器;
  8. get: 对象属性读取操作的拦截器;
  9. set: 对象属性赋值操作的拦截器;
  10. deleteProperty: delete操作符的拦截器;
  11. ownKeys: Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols的拦截器;
  12. apply: 函数调用操作的拦截器
  13. construct: new操作符的拦截器;

比如有 👇 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
const handler = {
get(obj, prop) {
return prop in obj ? obj [prop] : 37
}
}
const p1 = new Proxy({}, handler)
p1.a = 1
p1.b = undefined
console.info(p1.a, p1.b, p1.c) // 1, undefined, 37
// 无操作转发代理
const p2 = new Proxy({}, {})
p2.a = 1
console.info(p2.a) // 1

👆 上述例子中handler重新定义了对象的get动作,当从一个对象中获取属性时,如果属性存在则返回对应的值,不存在则返回默认的一个37数字。然后在p2中没有定义任何拦截操作,则说明将不做任何的逻辑处理!

😕 关于这个Proxy一般都有哪些应用场景呢? 👉 可以作为对象的验证器(通过重写set方法)、扩展构造函数(通过重写construct以及apply)等等!

Reflect

Reflect是一个内置的对象,它提供了拦截Javascript操作的方法,这些方法与上述的proxy_handler的方法一样,**Reflect不是一个函数对象**,因此它不可构造!
Reflect的所有属性和方法都是静态的(就像Math一样)
以下是关于Reflect的一个简单运用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
name: 'Koby',
age: 35
greeting() {
console.info(`Hello ${this.name}, my age is ${this.age}`)
}
}
Reflect.has(obj, 'name') // true
Reflect.has(obj, 'tip') // false
// 返回这个对象自身的属性
Reflect.ownKeys(obj) // ["name", "age", "greeting"]
// 为这个对象添加一个新的属性
Reflect.set(obj, 'tip', 'I like play basketball!')

使用Reflext可以让代码更加具有可读性和可维护性,因为它提供了一种统一的方式来调用基本操作,并且在操作失败时提供了更加友好的错误处理方式

reactive成员一览

vue3中的相关reactive成员的实现,则都是基于上述的Proxy以及Reflect来实现的!

对象响应式核心reactive()

reactive()接收一个对象参数,返回这个对象的响应式代理,通过该API所创建出来的对象是一个Proxy对象,一般只操作这个Proxy对象,而不会去使用原始对象。
以下是对应的一个简单例子:

1
2
const obj = reactive({ count: 0 })
obj.count ++

😕 而这个过程发生了什么事呢,下面来简单分析一波关于这个API的实现(隐藏非关联的代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// packages/reactivity/src/reactive.ts
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw',
}
// 包含自定义的以下参数的对象类型
export interface Target {
[ReactiveFlags.SKIP]?: boolean
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.IS_SHALLOW]?: boolean
[ReactiveFlags.RAW]?: any
}
export const reactiveMap = new WeakMap<Target, any>()
export multableHandler: ProxyHandler<object> = new MutableReactiveHandler()
export function reactive(target: object){
return createReactiveObject(
target,
// 这里隐藏关于这个readonly=false的参数
mutableHandlers,
// 这里隐藏关于复杂数据类型的handler参数
reactiveMap
)
}
function createReactiveObject(target: Target, handler: ProxyHandler<any>, proxyMap: WrakMap<Target, any>){
if(!isObject(target)){
if(__DEV){
warn('提示value必须为对象类型')
}
return target
}
if(对象已经被代理了){
return target
}
cosnt existingProxy = proxyMap.get(target)
if(existingProxy){
// 缓存中已存在该代理对象,则直接返回缓存中的代理对象
return existingProxy
}
if(对象非合法的类型){
return target
}
// 以下时真正创建一代理对象的动作
const proxy = new Proxy(target, handler)
proxyMap.set(target, proxy) // 将代理对象存储,便于后续缓存中读取
return proxy
}

🌟 通过开篇关于Proxy的使用我们可以得知,上述代码中的handler,必须是一个普通的对象,其所提供的属性对应方法将拦截了target对象的默认操作!

vue3中,凭借typescript,在其公共基础类型库中,通过自定义ProxyHandler<T extends object>接口,将handler中所提供的相关属性都罗列出来,为方便编写handler提供编写说明与提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface ProxyHandler<T extends object> {
// 这里时关于proxy中的handler所对应的所有方法定义
}
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly isShallow = false,
){
// 空实现的构造方法
}
get(target: Target, key: string | symbol, receiver: object) {
//! 这里隐藏其他属性的获取动作
const res = Reflect.get(target, key, receiver)
// 设置跟踪监听
track(target, TrackOpTypes.GET, key)
if(isObject(res)){
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
class MutableReactiveHandler extends BaseReactiveHandler {
constructor(isShallow = false) {
super(false, isShallow)
}
set(target: Target, key: string | symbol, value: unknown, receiver: object) {
let oldValue = target[key]
if(oldValue与value没有发生变化){
return true
}
const result = Reflect.set(target, key, value, receiver)
// 以下是对应的触发更新动作
if(key是新增的属性){
trigger(target, TriggerOpTypes.ADD, key, value)
}else if(key已存在于对象中){
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
// 以下隐藏关于deleteProperty、has、ownKeys方法的实现
}

🌟 上述通过结合BaseReactiveHandler以及MutableReactiveHandler,最终实现对getset方法的重写,来拦截相关的属性存取操作,通过在get获取属性时对该key设置监听,然后在set赋值属性时,触发监听动作,实现对对象属性的默认监听与拦截操作(这里是将触发对应的界面自动更新)!

😕 那么通过这个reactive()方法所创建出来的Proxy对象与其他对象有什么区别吗?
通过reactive创建出来的Proxy对象
👆 从上面我们可以看出所创建出来的Proxy对象拥有两个属性:target + handler,这与我们开头所习得的知识一样,然后从handler的继承关系来看,与源码的执行结果是一致的,继承关系如下图所示:
Proxy的handler的继承关系图

🤔 上述关于Target类型是什么呢?为什么要设计这样子的一个类型?

👉 首先从上述的代码我们发现关于这个Target就是一普通的对象,只不过它可能拥有着__v_skip__v_isReactive__v_isReadonly__v_isShallow__v_raw这几个属性中的一个或者多个属性,在上述的createReactiveObject方法中,我们用一句话:对象已经被代理了,来判断对象是否已被代理过,其对应的实现如下:

1
2
3
if(target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])){
return target
}

🌟 也就是说,通过reactive()所创建出来的代理对象,可通过ReactiveFlags.RAW以及ReactiveFlags.IS_REACTIVE来访问其值!
reactive创建出来的Proxy对象成员
😕 这里发现我们所传递的对象中是没有上面的这些属性的,但是通过Proxy,我们发现在其handler的get属性对应的方法中声明了对这些属性的访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BaseReactiveHandler implements ProxyHandler<Target>{
get(/*此处隐藏相关的参数*/){
const isReadonly = this._isReadonly, isShallow = this._isShallow
if(key === ReactiveFlags.ISREACTIVE){
return !isReadonly
}else if(key === ReactiveFlags.IS_READONLY){
return isReadonly
}else if(key === ReactiveFlags.IS_SHALLOW){
return isShallow
}else if(key === ReactiveFlags.RAW){
// 此处隐藏其他相关的判断
return target
}
}
}

🌟 这里自定义了对__v_isReactive__v_isReadonly__v_isShallow__v_raw这几个属性的访问,然后根据当前对象所采用的模式(通过reactive()readonly()shallowReactive()三者之一),来提供对应的get以及set操作!而且,还在对应的handler中保留了通过__v_raw的方式来访问原始对象(但这种方式尽量少用,而是使用toRaw),关于这个toRaw的方法定义如下:

1
2
3
4
export function toRaw<T>(observed: T):T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}

:face_holding_back_tears: 与toRaw相关的另外一个API是makeRaw,关于此方法的定义如下

1
2
3
4
5
6
7
export type Raw<T> = T & { [RawSymbol]?: true }
export function makeRaw<T extends object>(value: T):Raw<T>{
if(Object.isExtendsible(value)){
def(value, ReactiveFlags.SKIP, true)
}
return value
}

🌟 从方法中我们可以看出此方法通过接收一对象,然后将其转化成为不可响应式的对象, 🤔 这里为什么添加了这个ReactiveFlags.SKIP就可以认为它是一个不可响应的对象呢?
👉 我们在上述的代码中,利用伪代码对象非合法的类型,来代替对一个对象是否可代理的检测,关于此伪代码的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
function getTargetType(value: Target){
return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID : targetTypeMap(toRawType(value))
// 这里通过调用Object.toString()方法获取对象的类型,并通过string.slice(8, -1)获取到value的类型字符串
// 然后再调用targetTypeMap()映射出对应的可识别类型!
}

🤩 通过调用getTargetType(value)获取到对象的类型,如果是TargetType.INVALID的话,则说明是不可被响应式的对象!

shallowReactive()

reactive()的浅层实现,一般比较少用,因为它创建的树具有不一致的响应行为,可能很难理解与调试

isShallow()

检查传入的对象是否是一个shallow对象,该方法定义如下:

1
2
3
export function isShallow(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_SHALLOW])
}

isReactive()

检查一个对象是否由reactive()或者shallowReactive()创建出来的代理对象,该方法定义如下:

1
2
3
4
5
6
export function isReactive(value: unknown): boolean {
if(isReadonly(value)){
return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

isProxy()

检查一个对象是否由reactive()方法所创建出来的Proxy对象,该方法定义如下:

1
2
3
export function isProxy(value: any): boolean{
return value ? !!value[ReactiveFlags.RAW] : false
}

readonly成员一览

readonly()

接收一个对象或者一个ref,返回一个原value的只读代理,只读代理是深层的,对任何嵌套属性的访问都将是只读的!
关于此方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
export function readonly<T extends object>(
target: T,
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap,
)
}

🌟 readonly()的实现与reactive()的实现类似,就是传递的参数不一样而已,主要在于这个readonlyHandlers的实现不同,如下所示:

1
2
3
4
5
6
7
8
9
10
class ReadonlyReactiveHandler extends BaseReactiveHandler {
set(){
// 提示warning
return true
}
deleteProperty(){
// 提示warning
return true
}
}

🌟 从上面的handler我们可以发现,readonly()仅仅是实现了get方法,然后对set以及deleteProperty进行了直接返回,也就是不允许对对象的属性新型编辑操作!

isReadonly()

检查传入的值是否为只读对象,只读对象的属性可以更改,但他们不能通过传入的对象来直接赋值,这有点类似于const obj,而通过readonly()以及shallowReadonly()创建的代理都是只读的,因为它们在对应的代理对象的handler中将set方法给铲掉了!关于isReadonly()方法定义如下:

1
2
3
export function isReadonly(value: unknown): boolean {
return !!(value (value as Target)[ReactiveFlags.IS_READONLY])
}

🌟 通过访问一个对象中的属性__v_is_readOnly值是否为true来判断是否为一个readonly对象

shallowReadonly()

readonly()的浅层作用形式

总结

关于上述对reactive()以及其关联的其他API进行一个分析之后,发现当我们通过const state = reactive({count: 1})这个语句执行时,往被代理的对象中新增一系列自定义属性(__v_isXXX),代表正常响应式的、还是只读响应式的、还是浅层响应式的