Code前端首页关于Code前端联系我们

toRef是干啥的?

terry 2小时前 阅读数 6 #Vue
文章标签 toRef Vue

咱做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.ageprops.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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门