前言
说实话,看视频看的云里雾里,好像对于我这个vue的使用都没有很熟练的菜鸟来讲有些困难了😅,被50多分钟的视频硬控了快俩小时。但是好在自己反复对着代码查资料+问ai,也算是有点浅显的理解。
代码
废话不多说,直接上源码:
import { isObject } from '@vue/shared';
// 1.将数据转换成响应式数据,只能做对象类型数据的代理
const reactiveMap = new WeakMap(); //key只能是对象类型
// 实现同一个对象代理多次,返回同一个代理对象
// 代理对象的代理对象还是之前的代理对象
const enum ReactiveFlags { //响应式标识
IS_REACTIVE = '__v_isReactive',
}
export function reactive(target) {
if (!isObject(target)) {
return target;
}
if (target[ReactiveFlags.IS_REACTIVE]) { //如果target是代理对象,那么它一定会在这一步中进入get
return target; // 判定为代理对象,直接返回,对应多重代理问题
}
// 并没有重新定义属性,只是代理,在取值的时候会调用get,赋值时会调用set
let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 第一次普通对象代理,会通过new Proxy代理一次
// 下一次传入proxy对象,为了检测是否代理过,可以查看是否有get方法,有的话说明被proxy代理过
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 去代理对象上取值 使用get
// return target[key]; 这种方式this指向有问题
console.log(key);
// 可以监控到用户取值
const value = Reflect.get(target, key, receiver);
// Proxy要配合Reflect使用,保证this指向正确
// Reflect的recerver参数使this指向代理对象
// 如果获取的值是对象,递归调用 reactive 函数将其转换为响应式对象
if(isObject(value)){
return reactive(value);
}
return value;
},
set(target, key, value, receiver) {
// 去代理上设置值 使用set
// target[key] = value;
// 可以监控到用户设置值
return Reflect.set(target, key, value, receiver);
},
});
reactiveMap.set(target, proxy);
return proxy;
}
自己的一点理解
- 首先,
vue中的reactive大家都不陌生,它用于生成响应式数据,而且这个数据必须是对象类型。那么我们就先定义一个reactive函数,并规定参数只有一个并设置为对象(此处使用一个函数isObject()来判断)。 - 之后,我们使用
Proxy来生成一个原数据target的代理(const proxy = new Proxy(target,{...}),自定义setter和getter),此时有如下的代码:
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 去代理对象上取值 使用get
return target[key];
},
set(target, key, value, receiver) {
// 去代理上设置值 使用set
target[key] = value;
},
});
但是问题来了,这样写的话return target[key];返回的依旧是原对象的键值,并没有被代理包裹。
- 那么我们想到了使用专门用来配合
Proxy的Reflect,使用Reflect可以通过receiver参数指定this的指向,这里我们就可以让this始终指向代理对象:return Reflect.get(target, key, receiver),这样的话,当我们有如下代码:
function reactive(target) {
if (isObject(target)) {
return;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
},
});
return proxy;
}
// ----------------------------------------------------------
let target = {
name: '123',
get alias() {
console.log(this);
return this.name;
},
};
let proxy = reactive(target);
proxy.alias;
控制台有如下输出:
这说明我们已经实现了使用代理对象时this的正确指向。
- 现在问题又来了,如果我们将reactive对同一个对象使用两次,如:
const target = {
name:'aaa',
age:20
};
const A = reactive(target);
const B = reactive(target);
此时A与B相同吗,答案显然是否定的,因为我们使用了new关键字,二者占用了不同的内存空间,这造成了浪费。那么我们需要判断一下是否该对象已经被代理过,我们自然想到了Map,但是WeakMap更适合此处,因为它可以更好的被垃圾回收机制管理,所以我们决定使用它创建一个存储原对象 : 代理的键值对集合,也就是源码中的reactiveMap。每次使用reactive()都进行如下操作:
......
let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
......
reactiveMap.set(target, proxy);
......
这样我们就可以判断是否该对象已经被代理过,若代理过,则直接返回已有的代理,否则新建代理,避免了内存浪费。
- 另外,我们还想到一个特殊情况,假如一个对象被代理,然后对象的代理又被代理,如此嵌套,最终的结果怎么能保持只有一层
Proxy代理?这时我们需要一个变量做标记:
......
const enum ReactiveFlags { //响应式标识
IS_REACTIVE = '__v_isReactive',
}
......
if (target[ReactiveFlags.IS_REACTIVE]) {
//如果target是代理对象,那么它一定会在这一步中进入get
return target; // 判定为代理对象,直接返回,对应多重代理问题
}
......
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
},
});
......
因为Proxy在调用属性时一定会经过get,所以可以利用这一点进行判断。如果target是普通对象,那么在经过if语句时会跳过执行后方的正常流程,通过new来生成代理对象;而如果它本来就是代理对象,则会则if这一步因为读取ReactiveFlags.IS_REACTIVE键值而跳转到get内部,此时key等于该值,所以if语句为真,直接返回本就是代理对象的target。
结语
到这里就结束了,希望能对自己的代码水平有提高,之后也会继续学习。如果对您有帮助,欢迎帮我点赞,有问题也欢迎指正,虚心求教🙏。