Vue3中toRefs到底解决了什么问题?怎么用才不会踩坑?
先给刚转Vue3或者刚入门组合式API的朋友扫个小盲:组合式API里最常用的工具函数有两个“ref系”的代表,一个是直接处理单个值的ref,另一个就是今天要聊的toRefs,专门对着reactive对象“开刀”,很多人一开始会觉得ref和toRefs长得像兄弟,但用起来老出bug——一会儿数据不变了,一会儿结构出来的值散架了,这都是没搞懂它俩本质区别惹的祸。
toRefs解决的是reactive解构赋值后“丢失响应式”的核心痛点
咱们先看个非常经典的反例,很多新手刚用reactive就踩过这个坑:
import { reactive } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const user = reactive({
name: '张三',
age: 28
})
// 直接解构赋值
const { name, age } = user
const updateUser = () => {
name.value = '李四' // 这里会报错!name根本不是ref
// 就算你去掉.value,直接写name = '李四',页面上也不会变
user.age = 29 // 单独改reactive原对象,页面上原对象绑定的部分会变,但解构出来的age不会
}
return {
name,
age,
updateUser
}
}
})
为什么会这样?因为reactive创建的是普通JavaScript对象的“响应式代理”,代理对象的属性访问和修改会触发依赖更新,但它的单个属性本身不是ref,当你用ES6的对象解构把name、age从代理对象里取出来的时候,你拿到的只是原始数据的“快照值”——字符串和数字这种原始类型是按值传递的,取出来就和原代理对象切断联系了;哪怕是对象、数组这种引用类型,直接解构取出来后虽然能修改其内部属性,但整个变量本身替换掉(比如把解构出来的hobbies = ['旅游']改成hobbies = ['看书']),页面也不会更新。
这时候toRefs就该出场了:它的核心作用,是把reactive代理对象的每一个属性都转成独立的、指向原代理对象属性的ref引用,这样不管你是结构出来单独用,还是传给其他组件,修改这些ref的.value,都会同步修改原代理对象,同时触发依赖更新;反过来,修改原代理对象的属性,这些ref的.value也会跟着变,完全不会丢失响应式。
正确使用toRefs的基础姿势
刚才那个反例,用toRefs改一下就完美了:
import { reactive, toRefs } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const user = reactive({
name: '张三',
age: 28,
hobbies: ['旅游', '摄影']
})
// 用toRefs把整个user转成“属性都是ref的普通对象”
const userRefs = toRefs(user)
// 然后再解构,拿到的就是独立的ref引用了
const { name, age, hobbies } = userRefs
const updateUser = () => {
name.value = '李四' // 现在可以加.value修改了,页面会同步变
age.value = 29
hobbies.value[0] = '徒步' // 引用类型,不管是内部修改还是替换整个数组(hobbies.value = ['徒步'])都没问题
}
return {
name, // 这里直接返回解构的ref,模板里会自动解包,不用加.value
age,
hobbies,
updateUser
}
}
})
这里要注意一个小细节:模板里Vue3会自动解包顶层返回的ref,所以不管你是直接返回toRefs转后的userRefs,还是返回解构出来的name、age,模板里直接写{{ name }}、{{ age }}就可以,不用管.value;但如果是在setup函数内部、或者组件的props/emit之外的普通函数里用,就必须加.value,否则要么修改无效,要么拿到的是undefined或者快照值。
toRefs的进阶用法:别只会转整个reactive对象
很多人以为toRefs只能转完整的reactive对象,但其实它的“威力”不止于此——你还可以配合解构语法,只转你需要的几个属性,剩下的属性如果暂时不用响应式,或者是不想单独暴露给模板,可以保留在原reactive对象里。
import { reactive, toRefs } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const state = reactive({
name: '王五',
password: '123456', // 密码只在提交表单时用,不用单独暴露给模板
avatar: 'https://example.com/avatar.jpg',
// 下面是表单的其他状态
isLoading: false,
formError: ''
})
// 只转需要单独用的name、avatar、isLoading、formError
const { name, avatar, isLoading, formError } = toRefs(state)
const submitForm = async () => {
// 这里直接用原state对象的password,因为不用响应式更新
if (!state.password) {
formError.value = '请输入密码'
return
}
isLoading.value = true
try {
// 模拟提交接口
await new Promise(resolve => setTimeout(resolve, 1000))
alert('提交成功!')
} catch (err) {
formError.value = '提交失败,请稍后重试'
} finally {
isLoading.value = false
}
}
return {
name,
avatar,
isLoading,
formError,
submitForm
}
}
})
这样做的好处是按需暴露、按需维护,减少了模板里不必要的变量,同时也避免了整个state对象被意外修改带来的风险(不过原state对象本身还是响应式的,只是没暴露给模板,不会触发不必要的视图更新)。
容易混淆的三个点:toRef vs toRefs vs 直接转ref
toRef vs toRefs
toRef和toRefs名字只有一个字母之差,作用也很像,但区别很明显:
- toRefs是批量操作:它把一个对象的所有可枚举属性(包括原型链上的吗?不,只转自身的可枚举属性)都转成ref,返回的是一个普通对象;
- toRef是单个操作:它只把一个对象的指定属性转成ref,参数是(原对象,属性名,[默认值])三个,返回的是单个ref。
什么时候用toRef呢?比如你只想转reactive对象的某一个属性,或者这个属性一开始可能不存在(比如接口返回的数据里的某个字段是可选的):
import { reactive, toRef } from 'vue'
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const user = reactive({
name: '赵六'
})
// 接口可能返回address,也可能不返回,先用toRef设置默认值
const address = toRef(user, 'address', '北京市朝阳区')
const fetchUser = async () => {
// 模拟接口返回数据
const res = await new Promise(resolve => setTimeout(resolve => {
resolve({ address: '上海市浦东新区' })
}, 1000))
// 这里直接给address.value赋值,会自动把address添加到原user对象里,并且是响应式的
address.value = res.address
}
return {
user,
address,
fetchUser
}
}
})
如果用toRefs转的话,一开始user里没有address,转出来的对象里也不会有address属性,后期接口返回后再添加到user里,也不会自动成为ref;但用toRef就可以完美解决这个问题。
toRefs转的原对象必须是响应式的吗?
很多资料里说“toRefs的参数必须是reactive创建的响应式代理对象”,这句话其实不完全对——toRefs的参数可以是任何普通JavaScript对象,但如果参数不是响应式的,转出来的ref也是“非响应式的引用”:修改这些ref的.value,原对象的属性会变,但页面不会更新;反过来,修改原对象的属性,这些ref的.value也会变,但页面还是不会更新。
那什么时候要用toRefs转普通对象呢?比如你从父组件接收到了一个普通的props对象,想把它的属性转成ref传给子组件,或者是在某个工具函数里统一处理对象的属性,这时候可以用toRefs,但要记住不会有响应式效果。
直接用ref把reactive对象的属性包起来,和toRefs转的有什么区别?
这也是新手常问的问题:直接写const name = ref(user.name)不行吗?为什么要用toRefs?
区别在于是否和原对象保持引用关联:
- 直接用ref包的是原对象属性的“快照值”:比如user.name一开始是'张三',直接ref('张三')后,修改name.value只会改变这个新ref的值,原user对象的name不会变;反过来,修改原user对象的name,name.value也不会变;
- toRefs转的是原对象属性的“引用”:修改name.value,原user对象的name会同步变;修改原user对象的name,name.value也会同步变,两者是“双向绑定”的(当然这里的双向绑定不是指v-model那种,而是指数据层面的引用关联)。
实战场景中的toRefs:组件间传递响应式数据
除了刚才说的在setup函数内部解构赋值,toRefs在组件间传递响应式数据的场景里也非常好用,比如你有一个父组件,里面有一个reactive的form对象,想把它的几个属性传给子组件,同时希望子组件修改这些属性后,父组件的form对象也能同步更新:
父组件(Parent.vue)
import { reactive, toRefs } from 'vue'
import Child from './Child.vue'
export default {
components: { Child },
setup() {
const form = reactive({
title: '',
content: '',
tags: []
})
// 用toRefs转后解构,传给子组件
const { title, content, tags } = toRefs(form)
const submitForm = () => {
console.log('父组件收到的form数据:', form)
}
return {
title,
content,
tags,
submitForm
}
}
}
子组件(Child.vue)
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: { {
type: Object, // 注意:这里传过来的是ref,类型必须是Object,不能是String
required: true
},
content: {
type: Object,
required: true
},
tags: {
type: Object,
required: true
}
},
setup(props) {
// 子组件内部直接用props.title.value修改,父组件的form.title会同步变
const addTag = (tag) => {
props.tags.value.push(tag)
}
const removeTag = (index) => {
props.tags.value.splice(index, 1)
}
return {
addTag,
removeTag
}
}
})
这里要注意两个细节:
- 子组件接收ref类型的props时,类型必须写Object或Array(如果是数组的话),因为ref本身在JavaScript里就是一个普通对象,不能写String或Number;
- 如果子组件想用v-model绑定这些props的话,不能直接写v-model="title",因为props是只读的(虽然ref的.value可以修改,但直接绑定v-model还是会触发Vue的警告),这时候可以用computed创建一个双向绑定的计算属性:
import { defineComponent, computed } from 'vue'
export default defineComponent({ props: { { type: Object, required: true } }, emits: ['update:title'], // 可选,如果不用emit的话也可以直接改props.title.value,但用emit更符合Vue的规范 setup(props, { emit }) { const localTitle = computed({ get() { return props.title.value }, set(val) { // 两种方式都可以: // 1. 直接修改props的ref.value(Vue3.2+之后不会再警告,但规范上更推荐用emit) props.title.value = val // 2. 用emit通知父组件修改(更规范,符合单向数据流的原则) // emit('update:title', val) } })
return {
localTitle
}
用第一种方式的话,父组件不需要做任何处理,就可以直接同步更新;用第二种方式的话,父组件需要用v-model绑定:`<Child v-model:title="title" v-model:content="content" />`,这时候Vue会自动把update:title的事件处理函数绑定到title.value的修改上。
### 总结一下toRefs的使用注意事项
1. **toRefs是批量转属性为ref的工具**,专门解决reactive解构赋值后丢失响应式的问题;
2. **模板里自动解包顶层返回的ref**,不用加.value,但setup内部或普通函数里必须加;
3. **区别toRef和toRefs**:批量用toRefs,单个/可选属性用toRef;
4. **原对象如果不是响应式的,转出来的ref也不会触发视图更新**;
5. **直接用ref包属性和toRefs转的区别**:前者是快照值,后者是引用关联;
6. **组件间传递ref类型的props时**,类型要写Object/Array,v-model绑定要用computed创建双向计算属性。
最后再给大家提个醒:组合式API里的工具函数虽然很多,但不用刻意去背,遇到问题先想清楚“响应式是怎么工作的”,再去查对应的工具函数,这样就能很快上手了。 版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

