在 ref函数与reactive函数的对比 这一篇博文中,我们从使用角度对比了 ref 与 reactive 的区别,最终得出结论是,
- 通过 ref 定义的数据,在 js脚本中使用需要 xxx.value ,在模板中会自动解包,可以直接使用
- 通过reactive定义的数据,分为以下几种情况:
- 定义的是普通对象、普通数组、通过ref定义的响应式对象,在js和模板中可以直接使用
- 定义的是通过ref转化的数组、原生集合对象(Map对象),在 js脚本中使用需要 xxx.value ,在模板中会自动解包,可以直接使用
但是,在这里存在一个大家争议比较大的问题,那就是 使用 ref定义,在js中需要 xxx.value。使用 reactive 定义的数据,在模板中需要 xxx.属性。相比较Vue2的直接使用数据的模式,这样写起来都比较麻烦。例如下面的例子:
通过 reactive 定义数据
<template>
<p>姓名:{{ person.name }}</p>
<p>年龄:{{ person.name }}</p>
<p>薪资:{{ person.job.j1.salary }}</p>
<button @click="person.job.j1.salary++">薪资+1</button>
</template>
<script>
import {reactive} from "vue";
export default {
name: "TestComponent",
setup() {
let person = reactive({
name:'al',
age: 28,
job: {
j1: {
work: '前端',
salary:2
}
}
});
return {
person,
};
},
};
</script>
我们在 模板中每次都要通过 person.xxx 来使用属性,如果嵌套层级较深,就会像 person.job.j1.salary 一样,写起来太过繁琐。
想法一:直接返回具体数据
可能有人会有想法,那就是我在return 的时候,直接把数据返回出去,而不是返回整个对象,那会是什么情况?
<template>
<p>姓名:{{ name }}</p>
<p>年龄:{{ name }}</p>
<p>薪资:{{ salary }}</p>
<button @click="salary++">薪资+1</button>
</template>
setup() {
let person = reactive({
name:'al',
age: 28,
job: {
j1: {
work: '前端',
salary:2
}
}
});
return {
name:person.name,
age:person.age,
salary:person.job.j1.salary,
};
},
首先,如果我们直接返回具体数据,那么在模板中初始化肯定是可以直接使用的。
但是如果我点击按钮就会发现,页面完全不更新。
这是因为如果单独返回的是具体数据的话,其实是访问到了响应式对象 person 中的某个具体属性值,相当于返回的是一个具体的字符串,而一个字符串是不具备响应式的。
也就是说 return 对象中的 name:person.name 其实就是 name:'al'
name:person.name === name:'al'
在这里我们再来一个更加简单易懂的案例说明一下:现在有一个对象 person,我们对其操作
let person = {name:'al',age:28}
// 声明一个变量来接收 person 对象中的 name 属性
let newName = person.name // al
// 然后重新给 name 属性赋值
newName = '汤圆仔'
你说 源对象person 中的name 属性会发生改变么?那肯定是不可能的,因为这只是值的替换。
newName 更改之后,源对象 person根本没发生改变。
这和上面的例子其实是一个模式,那就是 newName 接收的其实是一个单纯的字符串类型的值,只不过这个值是从 源对象person 中取到的
想法二:用 ref 包裹具体返回数据
这个时候可能想法又来了,如果返回具体数据的同时,我用 ref 也将它转化为响应式怎么样呢?
return {
name:ref(person.name),
age:ref(person.age),
salary:ref(person.job.j1.salary),
};
可以看到,通过 ref 转化的数据,是可以实现响应式的,但是,真的和我们想的一样么?
按照我们的想法,当我们改变了数据,person 对象也肯定需要同步更改的,不然怎么叫响应式数据。
但是实际上,发现我们 person 对象并没有跟随变化。
这是因为在初始化时,确实是从 person 对象中取值,但是在取值过程中其实和上面的例子一样也只是拿到了一个基础类型的值,通过 ref 转化后,也只是将这个值转化为了响应式,而源对象 person 其实只在初始化时提供了一下值,后续操作毫无关联。
toRef 将数据转化为响应式
概念:可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
语法:toRef(响应式对象, "属性")
const state = reactive({
foo: 1,
bar: 2
})
// 双向 ref,会与源属性同步
const fooRef = toRef(state, 'foo')
打印 fooRef 我们可以看到,它其实也是一个 refImpl 响应式数据
同步:
// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2
// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3
所以基于 toRef ,我们可以实现想要的效果。
<template>
<p>姓名:{{ name }}</p>
<p>年龄:{{ name }}</p>
<p>薪资:{{ salary }}</p>
<button @click="salary++">薪资+1</button>
<p>{{ person }}</p>
</template>
return {
name: toRef(person, "name"),
age: toRef(person, "age"),
salary: toRef(person.job.j1, "salary"),
};
当我们点击按钮时,salary 会更改,此时 person 对象也会同步更改,实现同源。
相比于 ref(person.name) 是复制了源对象中的数据进行赋值,toRef(person.'name') 则是在访问 value 值时,通过 getter 指向了 源对象 person,本质是引用源对象。
此时,我们在模板上使用数据时,则可以正常使用。但是如果数据太多,每个都单独返回,那也太蠢了,所以 Vue3 提供了批量操作的API-- toRefs
toRefs 批量操作
概念:将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
语法:toRefs(响应式对象),不需要指定属性,因为转换的是一整个对象
let person = reactive({
name: "al",
age: 28,
job: {
j1: {
work: "前端",
salary: 2,
},
},
});
let newPerson = toRefs(person)
打印 newPerson ,我们发现确实是一个普通对象,但是对象内部的每个属性,都是单独的ref对象
而且对于深度嵌套数据,也实现了响应式。
因为 toRefs(person) 返回的是一个普通对象,所以在返回时我们需要通过扩展运算符进行解构
return {
...toRefs(person),
};
通过 toRefs 转化后,在模板文件中使用时,我们可以省去最外层的一层对象,而直接使用内部属性,例如:省去了 person 对象这一层,而是直接使用内部属性。
<template>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>薪资:{{ job.j1.salary }}</p>
<button @click="job.j1.salary++">薪资+1</button>
</template>
toRef 和 toRefs 的使用场景
在 setup 函数中,接收的 props 是响应式数据,此时我们只能通过 props.xxx 来使用其中的属性值,如果想要解构之后再使用,则会失去响应式
// 父组件 传递 props 给子组件
<template>
<Test :a='1' :b='2'></Test>
</template>
// 子组件通过props属性接收,同时在 setup 函数中可以接受到 props 属性接收的 数据
export default {
name: "TestComponent",
props: ["a", "b"],
setup(props, context) {
console.log(props, "props"); // Proxy(Object) {a: 1, b: 2}
function test(){
console.log(props.a); // 只能通过 props.xx使用
}
let { a } = props // 解构数据
function test(){
console.log(a); // 如果解构,那么此时 a 不具有响应式
}
return {
...toRefs(person),
};
},
};
如果你确实需要解构 props
对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:
export default {
name: "TestComponent",
props: ["a", "b"],
setup(props, context) {
let { a } = props // 解构数据
// 将 `props` 的单个属性转为一个 ref
const title = toRef(props, 'a')
// 或将 `props` 转为一个其中全是 ref 的对象,然后解构
const { title } = toRefs(props)
return {};
},
};