Vue.js设计与实现之十-原始类型的响应式代理

开发 前端
在本文中主要介绍了如何将原始值转为响应式数据,如何解决响应式丢失的问题,如何减少用户心智负担实现自动脱ref的能力等。

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数据的区分。

责任编辑:姜华 来源: 前端一码平川
相关推荐

2022-04-16 13:59:34

Vue.jsJavascript

2022-04-05 16:44:59

系统Vue.js响应式

2022-04-04 16:53:56

Vue.js设计框架

2022-04-09 17:53:56

Vue.js分支切换嵌套的effect

2022-04-01 08:08:27

Vue.js框架命令式

2022-04-25 07:36:21

组件数据函数

2022-04-12 08:08:57

watch函数options封装

2017-08-30 17:10:43

前端JavascriptVue.js

2021-01-22 11:47:27

Vue.js响应式代码

2022-04-26 05:55:06

Vue.js异步组件

2022-04-18 08:09:44

渲染器DOM挂载Vue.js

2022-04-11 08:03:30

Vue.jscomputed计算属性

2022-04-14 09:35:03

Vue.js设计Reflect

2022-05-03 21:18:38

Vue.js组件KeepAlive

2022-04-03 15:44:55

Vue.js框架设计设计与实现

2021-04-14 12:47:50

Vue.jsMJML电子邮件

2022-04-20 09:07:04

Vue.js的事件处理

2022-04-19 23:01:54

Vue.jsDOM节点DOM树

2021-09-18 10:07:23

开发技能代码

2016-11-01 19:10:33

vue.js前端前端框架
点赞
收藏

51CTO技术栈公众号