1、写在前面
在javascript中原始值包括:Boolean、String、Number、Null、Undefined、Symbol和BigInt等类型,原始值是按值传递而非按引用传递。前面,知道Proxy可以用于实现对象类型的响应式代理,但是却不能实现原始值的代理,要实现原始值变成响应式数据,就需要做些处理。
2、ref
Proxy的代理目标必须是对象类型,那么是否可以将原始值类型包装成对象类型,这样不就可以实现代理了吗?
// let name = "pingping"
const data = {
value: "pingping"
}
const state = reactive(data);
name.value = "onechuan";
想法是很好,但是你想过没有这样做带来的问题:
- 用户创建一个原始值的响应式数据,就必须创建一个包裹的对象。
- 而包裹对象又是由用户自定义,那么就存在命名和使用不规范情况。
解决方法很简单,你不是担心用户自定义的对象不规范不可控吗,那么就在源码内部定义不就行了。
function ref(val){
const wrapper = {
value: val
}
return reactive(wrapper);
}
简单试用下:
const refVal = ref("pingping");
effect(()=>{
console.log(refVal.value);
});
refVal.value = "onechuan";
但是,在使用过程中又有个问题:你又是如何保证refVal是原始值的包裹对象,还是一个非原始值的响应式数据呢?
const refVal = ref("pingping");
const refVal2 = reactive({value:"pingping"});
其实,ref和reactive生成的响应式数据实现方式都是一样的,对数据来源区分是不是ref是为了后续脱ref,脱出响应式能力恢复原始数据。
function ref(val){
const wrapper = {
value: val
}
Object.defineProperty(wrapper,"__v_isRef",{
value: true
})
return reactive(wrapper);
}
在上面代码中,使用Object.defineProperty给包裹对象wrapper定义一个不可枚举和不可写的属性"__v_isRef",使其值为true用于区分当前对象是ref而非普通对象。
简而言之:ref其实是对一个对象和reactive的二次封装。
3、响应丢失的问题
我们知道,ref可以用于实现原始值的响应式代理,但其实还可以用于解决响应式丢失的问题。所谓响应式丢失,就是在使用reactive生成的响应式对象数据,使用展开运算符(...)会丢失响应式,就成了一个普通对象数据。此时,修改修改对象的属性值,不会触发更新和模板渲染。
const obj = reactive({name:"pingping"});
const newObj = {...obj};
effect(()=>{
console.log(newObj.name);
});
obj.nmae = "onechuan";
在上面代码中,副作用函数中访问的只是普通对象newObj的属性name的值,它并不具有响应式能力,在对其属性值进行修改时,不会触发副作用函数重新执行。
那么,应该如何解决响应式丢失的问题呢?
其实就是能解决在副作用函数中,通过获取普通对象newObj的属性值,也会触发更新,与副作用函数建立联系。
通过在普通对象newObj中设置与obj对象同名的属性,将每个属性值都设置成对象,通过对象的get取值方法实现obj对象的属性值读取,这样就巧妙地将newObj的属性值与副作用函数建立了联系。
const obj = reactive({name:"pingping"});
const newObj = {...obj};
effect(()=>{
console.log(newObj.name);
});
obj.nmae = "onechuan";
但是,如果obj对象中有很多属性,那是不是就需要在newObj建立许多同名的对象?那么,就可以进行抽取封装函数:
function toRef(obj, key){
const wrapper = {
get value(){
return obj[key];
},
set value(val){
obj[key] = val
}
}
Object.defineProperty(wrapper,"__v_isRef",{
value: true
})
return wrapper;
}
在使用过程中,简简单单:
const obj = reactive({name:"pingping"});
const name = toRef(obj, "name");
name.value = "onechuan";
前面只是对少数对象的属性值转成响应式数据可以这样处理,但是当我们需要批量处理数据,应该如何处理呢?
很简单,对对象属性进行遍历不就得了。
function toRefs(obj){
const res = {};
for(const key in obj){
res[key] = toRef(obj,key);
}
return res;
}
这样,响应式丢失问题就被解决了,方法就是将响应式数据转换成类似ref结构的数据,通过toRef或toRefs转换后得到的数据就是真正的ref数据。
4、自动脱ref
使用toRefs用于解决响应丢失问题,就是对对象的属性进行遍历转为ref,这样就会带来新问题,就是去访问数据的第一层属性,必须通过.value才能访问。这样无疑会增加使用者的心智负担,用户肯定愿意直接对象.属性,而非通过对象.属性.value来使用属性值。
const obj = reactive({
name:"pingping",
age:18
});
const newObj = {
...toRefs(obj)
};
newObj.name.value//pingping
newObj.age.value//18
现在我们就需要让其自动脱ref,这样在进行对象属性的访问时,读取到属性是个ref则放回ref.value,否则直接返回属性值。
function proxyRefs(target){
return new Proxy(target,{
get(target, key, receiver){
const value = Reflect.get(target, key, receiver);
return value.__v_isRef ? value.value : value;
}
})
}
const newObj = proxyRefs(...toRefs(obj));
在上面代码中,通过定义一个proxyRefs函数接收一个对象参数,返回该对象的代理对象。而代理对象的作用是通过get操作,在读取到对象的属性是个ref值时,直接返回该ref.value值,否则直接返回属性值,这样就实现了自动脱ref。
其实,在模板中使用ref的属性值时,就是通过将组件setup返回的数据传递到proxyRefs函数中进行处理。这样就可以实现,在模板中直接访问属性值,而非属性.value值。
前面有实现自动脱ref的能力,现在就有实现自动穿ref的能力。实现原理,同样的是通过添加对应的set拦截函数。
function proxyRefs(target){
return new Proxy(target,{
get(target, key, receiver){
const value = Reflect.get(target, key, receiver);
return value.__v_isRef ? value.value : value;
},
set(target, key, newValue, receiver){
const value = target[key];
if(value.__v_isRef){
value.value = newValue;
return true
}
return Reflect.set(target, key, newValue, receiver);
}
})
}
5、写在最后
在本文中主要介绍了如何将原始值转为响应式数据,如何解决响应式丢失的问题,如何减少用户心智负担实现自动脱ref的能力等。ref本质就是一个包裹对象,通过reactive实现对原始值的响应式代理,但是包裹对象自爱本质上又和普通对象没啥区别,对此需要通过设置一个标识符__v_isRef来实现ref数据的区分。