目录
1.创建文件
在reactivity文件夹中创建reactive.ts文件,作为reactive的入口
在index.ts中将其统一导出
2.实现reactive
在reactive.ts中导出一个函数reactive,该函数接收一个对象作为参数,返回一个proxy代理对象
具体代码如下
export function reactive(target) {
return createReactiveObject(target) // 实现响应式函数
}
3.实现createReactiveObject
1. 判断参数是否为对象,如果不是则直接返回
function createReactiveObject(target) {
// 1.判断传入的值是否是对象,如果是对象正经行代理,如果不是直接返回
// isObject函数时当时在shared创建的一个判断是否是对象的函数,当时用于多个依赖包之间共享打包,在这里直接可以用于判断是否是对象
if(!isObject(target)) {
return target
}
}
2. 创建一个代理对象并返回
function createReactiveObject(target) {
// 1.判断传入的值是否是对象,如果是对象正经行代理,如果不是直接返回
// isObject函数时当时在shared创建的一个判断是否是对象的函数,当时用于多个依赖包之间共享打包,在这里直接可以用于判断是否是对象
if(!isObject(target)) {
return target
}
let proxy = new Proxy(target,mutableHandlers)
return proxy;
}
// 这里的代理对象的操作配置属性对象我们使用一个变量来存储
// 代理操作
const mutableHandlers: ProxyHandler<any> = {
get(target,key,recevier) { },
set(target,key,value,recevier) {
return true
}
}
这样一个基本的响应式变量就完成了。但是我们发现当两次同时传入同一个对象进行代理时,我们完全没有必要每次都在重新创建一个proxy,所以我们维护一个WeakMap,来存放每一个对象对应的proxy。
当两次需要代理的对象相同时,我们直接从WeakMap中获取proxy即可。
3. 实现WeakMap缓存
// 记录已经代理过的对象,避免重复代理
// 这里为了防止循环引用,所以用弱引用
const reactiveMap = new WeakMap()
在实现代理的函数中判断是否已经存在当前代理过的对象,如果有,直接将其返回。
function createReactiveObject(target) {
...
// 判断当前是否已经代理过,如果已经代理过直接返回
if(reactiveMap.has(target)) {
return reactiveMap.get(target)
}
...
}
在这里我们发现,当一个对象被代理之后,又重新将这个已经是响应式对象的对象再次传入,我们就不需要对其做任何操作,直接返回即可。
4. 判断target是否已经是响应式对象
这里用到了一个很巧妙的操作,我们在每次代理对象之前,从对象上读取一个标记属性,然后在每个代理对象的get方法中,专门判断这个标记属性
如果get方法判断到了这个属性,就说明这个对象在之前已经代理过,因此我们设置的get方法才能生效,否则就说明这个对象还没有被代理过,我们就可以进行代理操作。
enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive', // 已经代理过
}
function createReactiveObject(target) {
...
// 判断当前传入的值是否是一个响应式对象,如果是则直接返回
// 这里的判断方式是通过获取响应式对象的属性,然后再get方法中特定的返回,如果有返回,则代表其已经被代理了get方法
if(target[ReactiveFlags.IS_REACTIVE]) {
return target
}
....
}
const mutableHandlers: ProxyHandler<any> = {
// proxy get方法
get(target,key,recevier) {
// 判断当前是否已经是响应式对象
if(key === ReactiveFlags.IS_REACTIVE) {
return true
}
},
set(target,key,value,recevier) {
return true
}
}
4.实现get set方法中的返回值
- 这里我们现将上面所写的枚举ReactiveFlags,对象mutableHandlers移入一个新文件中,命名为baseHandler.ts
在将其导出,在reactive.ts中导入,实现分模块
- 这里我们需要在get和set方法中返回读取的数据(主要是在get方法中),但是我们却不能直接使用target[key]来读取数据,这主要涉及到一个问题,我们来看下面一个例子:
const user = {
name: "zhangsan",
get desc() {
return "我叫" + this.name
}
}
// 这里我们读取user.desc时,就会间接的读取this.name
let proxy = new Proxy(user,{
get(target,key,recevier) {
return target[key]
},
set(target,key,value) {}
})
我们来看上面的这个代码 在读取proxy.desc时,就会触发get方法,而desc间接依赖于name,在desc的逻辑当中,此时还是会读取this.name,而这个this还是user
这样一来就会出现一个问题,既然desc依赖于name,那么读取desc时就必须要读取name,这样才能将desc收集到name的effect,从而在未来name发生变化时,desc也会跟着变化
因此这样的方法可能会导致某些依赖我们无法收集到。那么使用recevier[key]行不行呢?我们知道recevier就是代理后的proxy对象,我们直接从代理后的对象上读取属性不是就可以触发name的get方法从而收集到依赖了
但是这里也有一个致命的问题,就是当我们在get内部去再去读取proxy的属性是,就会再次触发get方法,这样就会导致get一直被循环触发,最终导致请程序崩溃。
为了解决这一个问题,我们需要引入另一个API,Reflect :这个api就是专门用来解决循环读取属性的,它不会触发get方法,而是直接从proxy对象中读取属性。
get(target,key,recevier) {
// 判断当前是否已经是响应式对象
if(key === ReactiveFlags.IS_REACTIVE) {
return true
}
// 取值时,需要将响应式属性与effect进行绑定
return Reflect.get(target,key,recevier)
},
set(target,key,value,recevier) {
// 找到属性,然对应的effect触发执行
return Reflect.set(target,key,value,recevier)
},
这个api不会循环触发get,set方法,而是直接读取设置属性,这样就可以解决循环读取的问题。