toRef是干啥的?
咱做Vue3项目时,肯定碰到过响应式数据处理的事儿,比如从reactive对象里拿单个属性用,改的时候发现父对象没跟着变;或者组件传参后,子组件改值父组件没反应…这时候就得想到toRef啦!可toRef到底是干啥的?啥场景用?和ref、toRefs咋区分?今天咱一个个掰扯明白。
Vue3的toRef属于响应式API里的工具函数,作用是把响应式对象(reactive创建的)的某个属性,转成独立的ref对象,而且这个ref的value和源对象的属性是“绑定”的——改ref的value,源对象属性会变;改源对象属性,ref的value也会同步变。
举个例子:
import { reactive, toRef } from 'vue' const user = reactive({ name: '张三', age: 18 }) const nameRef = toRef(user, 'name') // 改nameRef的value nameRef.value = '李四' console.log(user.name) // 李四(同步变了) // 改user.name user.name = '王五' console.log(nameRef.value) // 王五(也同步变了)
要是不用toRef,直接拿user.name赋值给普通变量,那变量和user.name就没关系了,丢了响应式。const name = user.name
,改name不会影响user.name,因为name是普通字符串,不是响应式的。
啥场景下得用toRef?
场景1:组件传参时,父组件传reactive对象的单个属性,子组件想保持响应式
比如父组件这样写:
<template> <Child :age="user.age" /> </template> <script setup> import { reactive } from 'vue' const user = reactive({ age: 20 }) </script>
子组件如果直接接收 props.age
,props.age
是普通值(因为父组件传的是reactive对象的属性,传到子组件后,props里的age是“解包”后的值,不是响应式的),这时候子组件用toRef把 props.age
转成ref,改的时候才能同步给父组件:
<script setup> import { toRef } from 'vue' const props = defineProps(['age']) const ageRef = toRef(props, 'age') // 改ageRef.value,父组件的user.age会同步变 const handleClick = () => { ageRef.value++ } </script>
场景2:解构reactive对象时,想让单个属性保持响应式
比如有个reactive对象,想解构出来用,但又想让某个属性改的时候,原对象也改,这时候直接解构会丢响应式:
const user = reactive({ name: '张三', age: 18 }) const { name, age } = user name = '李四' // 这会报错,因为解构出来的name是普通值,而且reactive对象的属性不能直接赋值(得用user.name = '李四')
但如果用toRef处理单个属性,就能既解构又保持响应式:
const nameRef = toRef(user, 'name') const ageRef = toRef(user, 'age') // 之后用nameRef.value、ageRef.value,改的时候原user会同步变
对比toRefs:如果对象属性多,用toRefs一次性转所有属性更方便,但如果只需要单个属性,toRef更轻量。
场景3:抽离逻辑时,传递响应式属性的“引用”
比如写组合式函数(composable),需要把主逻辑里reactive对象的某个属性传给组合式函数,让组合式函数里的修改能同步到主逻辑,这时候用toRef把属性转成ref传过去,组合式函数里改ref.value,主逻辑的对象属性也会变。
toRef和ref、toRefs咋区分?
和ref的区别
- ref:创建全新的响应式变量,初始值由你传,和任何已有对象没关系。
const count = ref(0)
,count是独立的,改count.value不影响其他变量。 - toRef:基于已有的响应式对象(必须是reactive创建的)的某个属性,建立连接,源对象变,toRef的ref变;ref变,源对象也变。源对象必须是响应式的,否则toRef没用(比如普通对象用toRef,转出来的ref不是响应式的)。
举个错误示例:
const obj = { x: 1 } // 普通对象,不是reactive的 const xRef = toRef(obj, 'x') xRef.value = 2 console.log(obj.x) // 还是1,因为obj不是响应式的,toRef没起作用
所以toRef的前提是 源对象必须是reactive创建的响应式对象 ,否则白搭。
和toRefs的区别
- toRefs:把 整个reactive对象的所有属性 ,转成对应的ref,返回一个新对象(每个属性都是ref),适合批量处理对象的所有属性,比如解构时保持所有属性的响应式:
const user = reactive({ name: '张三', age: 18 }) const userRefs = toRefs(user) // userRefs是{ name: ref('张三'), age: ref(18) } const { name, age } = userRefs name.value = '李四' console.log(user.name) // 李四(同步变了)
- toRef:只处理 单个属性 ,更灵活,适合只需要某个属性保持响应式的场景,性能上也更优(不用遍历整个对象)。
总结下选择逻辑:
- 要全新的响应式变量 → ref - 要单个响应式对象的属性保持关联 → toRef(记得源是reactive的) - 要整个响应式对象的所有属性都转成ref → toRefs用toRef时容易踩的坑?
坑1:源对象不是reactive的,用toRef白忙活
前面举过例子,普通对象用toRef,转出来的ref不是响应式的,改了没效果,所以用toRef前,得确认源对象是 reactive({...})
创建的。
坑2:以为toRef能让普通变量变响应式
toRef不是用来把普通数据变成响应式的,它是“关联”已有响应式对象的属性,想让普通数据变响应式,得用ref。
坑3:在setup里错误处理props的单个属性
父组件传reactive对象的属性给子组件,子组件的props里的该属性是“解包”后的值,不是响应式的,这时候必须用 toRef(props, '属性名')
,而不是直接用 ref(props.属性名)
——因为ref(props.属性名)是把props.属性名的值作为初始值,创建新的ref,和props.属性名没关系了,改了不会同步。
比如错误写法:
const props = defineProps(['age']) const ageRef = ref(props.age) ageRef.value++ // 父组件的user.age不会变,因为ageRef是新的ref,和props.age没关系
正确写法是用toRef:
const ageRef = toRef(props, 'age') ageRef.value++ // 父组件的user.age会同步变
坑4:和v-model结合时的误解
比如子组件用toRef处理props的某个属性,然后在子组件里用v-model绑定这个ref,这时候要注意,v-model本质是value和onUpdate:modelValue,所以得确保ref和父组件的响应式属性正确绑定,不过只要toRef用对了,v-model是能正常同步的。
实战案例:用toRef解决组件传参的响应式问题
需求:父组件有个reactive的用户对象,包含name和age,把age传给子组件,子组件有个按钮,点一下age加1,父组件要同步显示变化。
父组件代码
<template> <div>父组件:用户年龄是{{ user.age }}</div> <Child :age="user.age" /> </template> <script setup> import { reactive } from 'vue' import Child from './Child.vue' const user = reactive({ age: 20 }) </script>
子组件错误写法(没用到Ref,导致点击没效果)
<template> <button @click="handleAdd">年龄+1</button> </template> <script setup> const props = defineProps(['age']) const handleAdd = () => { props.age++ // 报错!因为props是只读的,而且age是普通值,不是响应式的引用 } </script>
子组件正确写法(用toRef保持响应式)
<template> <button @click="handleAdd">年龄+1</button> </template> <script setup> import { toRef } from 'vue' const props = defineProps(['age']) const ageRef = toRef(props, 'age') // 把props.age转成ref,和父组件的user.age关联 const handleAdd = () => { ageRef.value++ // 改ref的value,父组件的user.age会同步变 } </script>
这样点击按钮,父组件的user.age就会+1,页面同步更新。
再举个解构reactive对象的案例:
需求:有个reactive的购物车对象,包含total(总金额)和count(商品数量),要解构出来,但修改total时要同步更新原对象。
import { reactive, toRef } from 'vue' const cart = reactive({ total: 100, count: 2 }) // 错误:直接解构,丢响应式 const { total, count } = cart total = 200 // 报错,因为reactive对象的属性不能直接赋值,而且解构后total是普通值 // 正确:用toRef处理total const totalRef = toRef(cart, 'total') totalRef.value = 200 console.log(cart.total) // 200,同步更新了
toRef在组合式函数中的应用
假设写一个处理用户信息的组合式函数useUser,需要接收外部reactive对象的name属性,在组合式函数里修改name,外部能同步更新。
组合式函数useUser.js
import { toRef, watch } from 'vue' export function useUser(nameRef) { // 监听nameRef的变化,做些逻辑(比如发请求) watch(nameRef, (newName) => { console.log(`名字改成了${newName}`) // 这里可以加其他逻辑,比如更新用户信息到后端 }) const changeName = (newName) => { nameRef.value = newName } return { changeName } }
组件里使用
<template> <button @click="changeName('李四')">改名字</button> </template> <script setup> import { reactive, toRef } from 'vue' import { useUser } from './useUser.js' const user = reactive({ name: '张三' }) const nameRef = toRef(user, 'name') // 把user.name转成ref传给组合式函数 const { changeName } = useUser(nameRef) </script>
这样,在组合式函数里调用changeName,能同步修改user.name,而且watch也能监听到变化,实现逻辑解耦又保持响应式关联。
toRef是Vue3响应式工具里的“连接器”,专门解决 单个响应式属性的关联传递 问题,记住这几点:
- 作用:让reactive对象的单个属性,以ref的形式保持响应式连接;
- 场景:组件传参单个属性、解构单个属性保持响应式、组合式函数传递属性引用;
- 区别:和ref(全新创建)、toRefs(批量处理)的适用场景不同;
- 避坑:源必须是reactive对象,否则无效;处理props单个属性时别用错方法。
实际开发中,只要涉及“拿响应式对象的单个属性用,还想改的时候同步回去”,就该想到toRef,把这层逻辑理顺,Vue3的响应式处理能少踩很多坑~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。