前言
在vue
更新到3.0版本之后,推出了组合式 写法,在组合式 写法中定义响应数据需要引入api
来定义,但是vue3
中却提供了ref
和reactive
两个api
来定义响应数据,让开发者的选择变得更多。
但是发现很多vue3
的开发者却对这两个api
的选择很是纠结,甚至有在组合式 中以选项式 的方式定义响应数据,那这样定义响应数据还符合vue3组合式 开发的设计思想吗?这两个api
的选择真的有必要纠结吗?使用这两个api
定义响应数据心理负担真的很大吗?
正文
先说一下两个ref
和reactive
的使用和差别
ref
ref
接受一个任意类型的值,返回一个相应可更改的ref
对象,该对象只有一个指向内部值得属性value
。如果接受的参数是一个引用类型值,那么这个值将通过 reactive
转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref
,它们将被深层地解包。
所以在我们使用ref
定义响应数据之后,访问数据都需要.value
才能拿到定义的那个响应数据。更改也需要更改.value
的值
js
const name = ref("iceCode");
console.log(name.value);//iceCode
name.value = "icedCode";
console.log(name.value);//icedCode
const form = ref( {
value: 1,
name:'iceCode'
} )
//这里访问这个对象仍需要.value之后再访问对象中的属性
console.log( form.value.value );//1
form.value.value = 12138
console.log( form.value.value );//12138
console.log(form.value.name);//iceCode
reactive
reactive
只接收引用类型值,返回一个代理的响应数据。它是一个深层次的代理,会影响到所有嵌套的的属性,并且解包会ref
对象,并保持响应性,但是由ref
定义数组或Map
的响应数据是不会自动解包的。
js
//如果在reactive中传入一个值类型的数据,那么就会收到一个警示,并且不会生效为一个响应数据
const title = reactive("标题");// value cannot be made reactive: 标题
const form = reactive({
title: "标题",
});
const arr = reactive([1, 2, 3, 4, 5]);
const refTitle = ref("标题1");
const content = reactive({
name: "iceCode",
arr,
title: refTitle.value,
form,
});
console.log(content);
这里看出得到,使用reactive
定义得响应对象直接是一个Proxy
对象,并且对象属性得值如果是一个引用类型,依旧是一个Proxy
对象
关于解包
关于"深层的解包",这里的意思是指,如果一个对象被赋值给ref
,并且这个对象内部还包含其他的ref
,那么这些内部的ref
也会被解包并转换为响应式的对象。这个过程就是所谓的"深层的解包"。
举个例子来说明,假设有一个对象obj
,它包含一个嵌套的对象nestedObj
,而nestedObj
还有一个ref
属性。那么当我们把obj
赋值给一个ref
时,这个ref
不仅会解包obj
,还会进一步解包nestedObj
和它的ref
属性。这样,如果我们对obj
或nestedObj
进行操作,那么相关的响应式副作用也会被触发。
这种"深层的解包"机制使得Vue可以更好地处理嵌套的数据结构,并且可以更准确地追踪变化并触发相应的响应式行为。
ref和reactive得差异及使用
上面讲到ref
和reactive
得时候也差不多都说了。访问ref定义的响应数据时必须需要.value
才能拿到定义的那个值,当然这个也无需担心每次访问ref
定义的响应数据的时候都要手动.value
,使用vue3
得开发者肯定也已经安装了Vue Language Features (Volar)
这个插件,如果你访问得变量是由ref
定义的响应数据,它会自动在后面补充上.value
的。
因为reactive
定义的响应数据无法是基本类型的数据,所以很多情况下灵活度不如ref
,但在访问reactive
定义的响应数据的时候,无需使用再多一步.vaule
的操作,只需要直接访问属性或者索引值即可,在访问一个响应的对象或数组上明显方便并且页面也清爽了不少。
另外reactive 定义的响应数据无法重新赋值,否则会丢失响应性。但ref
的.value
重新赋值却不会产生这种情况。为什么reactive重新赋值会造成响应式的丢失呢?
主要原因就是因为reacitve
定义的响应数据是由Proxy
直接代理的对象,当重新赋值之后,原来的引用地址改变了 (这里需要熟悉js里的值类型和引用类型的存储方式),当给reactive
重新赋值之后,这个变量虽然没变,但是指向堆中的地址改变了,而Proxy
代理的地址还是原来那一个,所以当再重新赋值的那一刻,reactive
的响应数据就丢失了响应性。
js
let obj = {
name: "iceCode",
};
//这里Proxy返回的对象,就是obj代理后的对象,必须要有返回值
const pObj = new Proxy(obj, {
//当访问pObje的属性时会触发此方法
get(target, key) {
console.log("get", key);
return Reflect.get(target, key);
},
//当修改pObje的属性时会触发此方法,必须要有返回值
set(target, key, value) {
console.log("set", key, value);
target[key] = value;
return Reflect.set(target, key, value);
},
});
//当我们在这更改pObj的数据时,obj的数据会同步更改
pObj.name = "前端开发";
//这里再node里运行的 为了更好的看出来 所以就这样写了,浏览器中正常打印就可以了
console.log(`obj:${JSON.stringify(obj)},
pObj:${JSON.stringify(pObj)}`);
得到结果打印了多次,set
那个时在pObj
修改属性值的时候访问了set
方法,所以打印了一次,后面两次get是因为转JSON的时候调用了一次,模板字符串又调用了一次(直接console.log变量是不会访问get方法的) ,最后的才是最终打印的结果。最终的结果得出在pObj
修改属性之后,obj
确实是同步更改了
js
//...
//在修改obj之前再打印一遍
console.log(obj, pObj);
//这里给obj重新赋值一个对象,然后再次修改pObj的属性值
obj = { name: "3" };
pObj.name = "前端开发";
console.log(`obj:${JSON.stringify(obj)},
pObj:${JSON.stringify(pObj)}`);
当obj
重新赋值一个对象之后,pObj
再次修改属性,它们之间就不再相互影响了。
反过来修改obj
的属性也是一样的
js
//...
//修改obj的属性值
obj.name = "前端开发";
console.log(obj, pObj);
//当给obj重新赋值一个对象后,再次修改属性值
obj = { name: "3" };
obj.name = "切图仔";
console.log(`obj:${JSON.stringify(obj)},
pObj:${JSON.stringify(pObj)}`);
修改obj
的属性时是不会触发Proxy
代理里的set
方法的,其余的效果跟pObj
是相同的。所以这里就可以理解了为什么reactive
定义的响应数据不可以重新赋值了。
想看Proxy的详细使用可以看我的另外一篇文章:不会Reflect,怎么玩转Proxy --Proxy篇 - 掘金 (juejin.cn)
如果真的有需求要重置一个由reactive定义的响应数据,该怎么办呢?
使用Object.assign()
方法
Object.assign()
方法是浅拷贝对象,接收多个参数并将第二个参数之后的对象属性赋值到第一个参数里内。如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。这里的源对象就是第一个参数,这种复制不会改变引用地址,所以也一定不会破坏使用reactive
定义响应数据的响应性。
js
//假设定义了一个表单数据
const form = reactive({
name: "iceCode",
age: 18,
phone: "18888888888",
site: "成都",
});
//这里深拷贝一下表单数据,方便之后重置数据的时候使用
const formClone = structuredClone(toRaw(form));
//这里假设修改了数据
form.name = "切图仔";
form.age = 24;
form.phone = "100068";
form.site = "郑州";
//打印之后肯定是生效了,form表单内的数据被修改了
console.log(form);
//使用Object.assign 这个表单被重置了
Object.assign(form, formClone);
//打印之后表单也被重置成功了
console.log(form);
看出Object.assign()
是可以修改reactive定义的响应数据的,深拷贝那里为什么要使用toRaw可以看另外一篇:以响应式著称的Vue,为什么会提供toRaw和markRaw - 掘金 (juejin.cn)
js
//...
//当然也可以使用keys方法拿到key的数组,然后遍历
Object.keys(form).forEach((key) => {
form[key] = formClone[key];
});
//或使用for in
for (const key in form) {
form[key] = formClone[key];
}
console.log(form);
在需要给这个变量重新赋值的场景下,reactive
定义的响应数据不如使用ref
,但也确实使用reactive
不会让页面中出现大量的.value
。
reative和ref的使用场景
说了这么多reactive
和ref
,乍看上去如果混合使用还是挺混乱的,但在实际项目里也许一个组件要写好多代码,需要一些提出去一些代码或者代码解耦的情况下,两者使用并不冲突,也不会有任何的心里负担。
秉承着组件化的思想,一个页面也许会有许多组件,但有时候每个组件也许都会使用到一组数据,提出去的响应数据完全可以使用reactive
,而在组件内的定义一个弹窗显示隐藏的Flag
,定义一个title
等这种基本数据类型的都可以使用ref
来定义(当然这样是写一步算一步的情况下,如果一开始就是一步相好的,完全可以使用reactive
定义一个对象来使用),并且更符合vue3
的组合式设计思想。
一个简单的例子:
js
//hooks/home.js 这里是hooks里的home文件
import { reactive } from "vue";
导出定义的表单数据
export const form = reactive({
name: "iceCode",
});
//Home.vue 这里是Home文件
import { form } from '../hooks/home.js'
//深拷贝form表单数据 当然也可以写在home.js文件里 导出来
const formClone=structuredClone(toRaw(form))
/* 弹窗 */
//弹窗显示隐藏
const modelFlag = ref( false )
const modelClick = () => {
modelFlag.value = !modelFlag.value
}
/* 重置 */
//表单重置
const resetForm = () => {
Object.assign(form, formClone)
}
/* 获取数据 */
const tabData=ref([])
//列表数据获取
const getData = () => {
fetch( 'getDate' ).then( res => res.json() ).then( res => {
tabData.value=res.data
})
}
//进入页面获取数据
//这里onMounted本身就是接收一个回调函数,而getData也正好是个函数,所以可以这样简写
onMounted( getData )
//HomeBody.vue 这里是Home的子组件 HOmeBody文件
//在子组件内需要的时候就可以直接引入
import { form } from "../hooks/home.js";
form是响应式数据,不论是在哪个文件内改变form内的数据,另外文件内在访问form的时候,都是被修改过的并且是实时响应的
结尾
说到最后也是想说在写vue3
项目的时候,虽然它提供了ref
和reactive
两个api
来定义响应数据,并且还是有些许差别,但在使用上无需任何心理负担。按照组件化思想以及vue3
的组合式思想,便可轻松合理的使用它们。
ref
本就是reactive
的包装版,在引用类型数据下,最终还是调用的reactive
。并且ref
的.value
也并不是一个痛点,使用Vue Language Features (Volar)
插件后也跟没有这个.value
差不多,也无需抵触。
另外也不太建议在组合式中使用一个reactive
将所有响应数据全写里面,那样还不如直接写选项式(当然每个人有自己兴趣,自己写着顺手就行,无需听他人多言)
最后可以说说你在vue3组合式中定义响应数据的痛处是什么,或者说在定义数据的时候有没有的选择困难症(心里负担是什么)