2024-7-5
关于Reactive
官方介绍:
reactive ()
返回一个对象的响应式代理。
- 类型
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
- 详细信息
- 响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。
- 值得注意的是,当访问到某个响应式数组或
Map这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。- 若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。
- 返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。
实现Reactive
从官方描述来看,Reactive本质上就是将对象利用Proxy进行代理,生成一个代理对象,从而实现响应式。
话不多说,先实现一个简单的代理对象吧!
1.简单的代理对象
// reactive 方法
export function reactive(target) {
// 调用创建响应式对象方法 统一处理
return createReactiveObject(target)
}
// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
return true
}
}
// 创建响应式对象方法
function createReactiveObject(target){
// * * * * * * * * *
// 判断当前是否为对象
// 若不是对象 直接返回 不做响应式
// 例:reactive(123) => 123
if (!isObject(target)) {
return target
}
// * * * * * * * * *
// 将当前对象 利用Proxy方法 做代理
let proxy = new Proxy(target,mutableHandlers)
return proxy
}
此时,一个简单的reactive方法就实现了,我们可以利用此方法创建一个Proxy对象。
// 功能实现:
// 1、只对 对象类型 做响应式
let obj = reactive(123)
console.log(obj); // 123
let obj1 = reactive({name:'ywh'})
console.log(obj); // 此时obj为Proxy对象
2.避免重复代理
刚才实现的是最基本的reactive方法,但我们需要考虑一种情况———重复代理。
也就是将同一对象做多次代理。
let obj = {name:'ywh'}
let proxyObj1 = reactive(obj)
let proxyObj2 = reactive(obj)
重复代理,是一件浪费性能的事情,而且也没有必要。
那该如何避免重复代理呢?
一种思路是,将当前对象与其代理对象建立映射关系,并且缓存起来。
每次创建代理对象时,先去缓存表里看看是否有当前对象,若有,说明已经缓存过,即当前对象已经代理过了,那此时就无需再次代理,直接将对应缓存的代理对象返回即可。
// 缓存 对象 与其 代理对象 的映射
// 用于 避免重复创建代理对象
const reactiveMap =new WeakMap()
export function reactive(target) {
// 调用创建响应式对象方法 统一处理
return createReactiveObject(target)
}
// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
if (key ===ReactiveFlags.IS_REACTIVE ) {
return true
}
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
return true
}
}
// 创建响应式对象方法
function createReactiveObject(target){
// 判断当前是否为对象
// 若不是对象 直接返回 不做响应式
// 例:reactive(123) => 123
if (!isObject(target)) {
return target
}
// * * * * * * * * *
// 新增逻辑:
// 判断当前对象 是否已经代理过
// 即:判断当前对象是否在reactiveMap缓存过
const exitsProxy = reactiveMap.get(target)
if (exitsProxy) {
// 若缓存过
// 返回其代理对象
return exitsProxy
}
// * * * * * * * * *
// 若是对象 则对其做代理
// 普通对象 => 代理对象
// target:目标对象 mutableHandlers:代理逻辑
let proxy = new Proxy(target,mutableHandlers)
// 代理完成后
// 将当前对象 及其 代理 缓存
reactiveMap.set(target,proxy)
return proxy
}
从而,我们可以实现避免重复代理功能
// 功能实现:
]// 3、实现缓存 避免重复代理
let obj = {name:'ywh'}
let proxyObj1 = reactive(obj)
let proxyObj2 = reactive(obj)
console.log(proxyObj1 === proxyObj2); // true
3.避免嵌套代理
除了重复代理(同一对象多次代理)的情况,我们也还需要考虑代理对象再次代理的套娃情况,即嵌套代理。
这也就意味着,我们需要识别出当前对象是代理对象,还是未代理的普通对象。
根据我们当前已写的逻辑,代理对象只不过是利用Proxy方法,将普通对象做一层代理,当我读取或者修改此对象属性时,触发get和set方法。不过如此~~~
既然如此,那我们是不是可以在代理对象的get方法里做文章??
对于普通对象而言,访问其属性,就只是单纯在其身上寻找该属性;而代理对象,访问其属性,会触发其get方法。
那么我可以设计一个全局唯一的属性名!
普通对象访问此属性时,值一定为空,布尔值类型为false;
而代理对象访问此属性,一定会进入get方法,我可以在get方法里做个判断,若当前访问的是全局唯一的属性名,则返回布尔值true。
// * * * * * * * * * * * *
// 新增逻辑:
// 设计一个全局唯一的属性名
// 代理标记
enum ReactiveFlags{
IS_REACTIVE="__v_isReactive",
}
// * * * * * * * * * * * *
export function reactive(target) {
// 调用创建响应式对象方法 统一处理
return createReactiveObject(target)
}
// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
if (key ===ReactiveFlags.IS_REACTIVE ) {
return true
}
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
return true
}
}
// 缓存 对象 与其 代理对象 的映射
// 用于 避免重复创建代理对象
const reactiveMap =new WeakMap()
// 创建响应式对象方法
function createReactiveObject(target){
// * * *
// 判断当前是否为对象
// 若不是对象 直接返回 不做响应式
// 例:reactive(123) => 123
if (!isObject(target)) {
return target
}
// * * * * * * * * * * * *
// 新增逻辑:
// 判断当前对象是否为代理对象
// 普通对象一定为null 而代理对象则会进入get回调函数内
// 此时可以在get回调函数内 处理相关逻辑
if (target[ReactiveFlags.IS_REACTIVE]) {
// 若当前对象为代理对象
// 则直接返回 不必再做代理
return target
}
// * * * * * * * * * * * *
// 判断当前对象 是否已经代理过
// 即:判断当前对象是否在reactiveMap缓存过
const exitsProxy = reactiveMap.get(target)
if (exitsProxy) {
// 若缓存过
// 返回其代理对象
return exitsProxy
}
// 若是对象 则对其做代理
// 普通对象 => 代理对象
// target:目标对象 mutableHandlers:代理逻辑
let proxy = new Proxy(target,mutableHandlers)
// 代理完成后
// 将当前对象 及其 代理 缓存
reactiveMap.set(target,proxy)
return proxy
}
创建一个全局唯一的属性,并且利用代理对象的性质,从而可以避免嵌套代理。
// 功能实现:
// 4、避免嵌套代理
let proxyObj = reactive({name:'ywh'})
let ProxyAgainObj = reactive(proxyObj)
console.log(proxyObj === ProxyAgainObj); // true
此时,当前文件略显臃肿,我们可以将代理逻辑以及代理标记抽出去,方便后续增加新方法。
创建baseHandler.ts文件
//baseHandler.ts
// 代理标记
export enum ReactiveFlags{
IS_REACTIVE="__v_isReactive",
}
// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
if (key ===ReactiveFlags.IS_REACTIVE ) {
return true
}
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
return true
}
}
4.实现代理逻辑
实现代理逻辑的get和set方法
一种很自然的想法是:
get()方法返回对象的key属性,即target[key] ,
set()方法设置对象属性为新传入的value值,即target[key]=value
// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
if (key ===ReactiveFlags.IS_REACTIVE ) {
return true
}
return target[key]
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
target[key]=value
return true
}
}
但这样会存在一个问题!!!
例如如下代码:
// 创建一个person对象
// 对象内有一个name属性和一个getter方法
const person ={
name:'ywh',
get outputName(){
return this.name + 'is good man!'
}
}
// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
get(target,key,receiver){
console.log(key);
return target[key]
}
})
// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// => 触发get方法
// => Output: outputName 输出key
// => 读取原对象上的outputName属性
// => 此时 此方法this指向person对象
// => Output: ywh is good man! 输出
console.log(proxyPerson.outputName);
看起来,好像没问题?
确实输出了想要的结果。
但是仔细观察会发现,执行outputName方法时,访问了person对象的name属性,但是name属性并没有触发get方法。
原因在于outputName方法中的this指向的是person对象,而非代理对象proxyPerson,从而不会触发get方法。
这有很大的问题!!
这意味着,当我后续修改name属性时,不会触发proxyPerson.outputName依赖的effect,会丢失响应式!!
那我们尝试修改代码,返回代理对象proxyPerson身上的outputName方法,而非原对象身上的outputName方法。
// 创建一个person对象
// 对象内有一个name属性和一个getter方法
const person ={
name:'ywh',
get outputName(){
return this.name + 'is good man!'
}
}
// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
get(target,key,receiver){
console.log(key);
return receiver[key]
}
})
// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// => 触发get方法
// => Output: outputName 输出key
// => 读取代理对象上的outputName属性
// => 触发get方法
// => Output: outputName 输出key
// => 读取代理对象上的outputName属性
// => 触发get方法
// => Output: outputName 输出key
// => 读取代理对象上的outputName属性
// => ......
console.log(proxyPerson.outputName);
糟糕!!!
我们发现程序加入了死循环!!!
每次读取代理对象上的outputName属性时,都会触发get方法;
而get方法又会读取代理对象上的outputName属性。
从而陷入了死循环 T^T.
我们尝试了从原对象身上找属性(会导致丢失响应式),以及从代理对象身上找属性(会陷入死循环),结果都不如意。
那我们想想,能不能够还是从原对象身上找属性,但是修改属性内的this为代理对象呢?
我们试试看!
利用Reactive.get()方法,实现从原对象中读取属性,但修改this为代理对象。
来自MDN介绍:
Reflect.get() 方法与从对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。
参数
target: 需要取值的目标对象
propertyKey: 需要获取的值的键值
receiver: 如果target对象中指定了getter,receiver则为getter调用时的this值。
// 创建一个person对象
// 对象内有一个name属性和一个getter方法
const person ={
name:'ywh',
get outputName(){
return this.name + 'is good man!'
}
}
// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
get(target,key,receiver){
console.log(key);
return Reflect.get(target,key,receiver)
}
})
// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// => 触发get方法
// => Output: outputName 输出key
// => 读取原对象上的outputName属性 修改其中this为代理对象
// => 执行原对象上的outputName方法
// => 读取this.name
// => 触发get方法
// => Output: name 输出key
// => 返回原对象的name属性值
// => 返回 ywh is good man!
// => Output:ywh is good man! 输出最终结果
console.log(proxyPerson.outputName);
此时,发现outputName和name都触发了get方法,从而实现了响应式!!!
We get it ~~~
所以此时修改一下代理逻辑的get和set方法
// 代理标记
export enum ReactiveFlags{
IS_REACTIVE="__v_isReactive",
}
// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
// 读取对象属性时的回调方法
get(target,key,receiver){ //receiver 指向代理后的对象
if (key ===ReactiveFlags.IS_REACTIVE ) {
return true
}
// Reflect作用 => 用于修改this指向
// 若target[key] 是一个getter函数
// 可以让其中的this 指向receiver
return Reflect.get(target,key,receiver)
},
// 设置对象属性时的回调方法
set(target,key,value,receiver){ //receiver 指向代理后的对象
// Reflect作用 => 用于修改this指向
// 若target[key] 是一个setter函数
// 可以让其中的this 指向receiver
return Reflect.set(target,key,value,receiver)
}
}
从而,我们实现了代理逻辑 get 和 set
// 功能实现:
// 5、触发代理逻辑 get 和 set
let proxyObj = reactive({ name: 'ywh' })
console.log(proxyObj.name); // 此时会调用get方法
至此!!
我们实现了Reactive方法,可以实现将普通对象进行代理,从而可以方便后续实现响应式。