Vue 3 中 toRef 和 toRefs 的使用
FreeGuideOnline
最新
2026-07-07
js import { reactive } from 'vue'
const state = reactive({ name: 'Vue', count: 0 })
如果直接解构:
```js
const { name, count } = state
// 修改解构后的变量
name = 'React' // 不会触发 state.name 的变化
count++ // 不会触发 state.count 的变化
解构后得到的是原始值的拷贝,和源对象失去了响应式连接。这会导致视图无法更新,逻辑难以维护。
toRef 和 toRefs 正是为了解决这个问题。
toRef:保持单个属性的响应性
基本语法
function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): ToRef<T[K]>
toRef 接收两个参数:
object:一个响应式对象(由reactive创建或嵌套在响应式对象内)key:对象的属性名
返回值是一个 Ref,对该 ref 的读写会直接映射到源对象的相应属性上。
使用示例
<script setup>
import { reactive, toRef } from 'vue'
const user = reactive({
name: 'Alice',
age: 25
})
// 创建一个与 user.name 保持同步的 ref
const nameRef = toRef(user, 'name')
// 修改 ref 会更新源对象,反之亦然
function changeName() {
nameRef.value = 'Bob' // user.name 也变为 'Bob'
// user.name = 'Charlie' // nameRef.value 也会同步为 'Charlie'
}
</script>
<template>
<p>姓名:{{ nameRef }}</p>
<p>原始对象姓名:{{ user.name }}</p>
<button @click="changeName">修改姓名</button>
</template>
在模板中可以直接使用 nameRef(自动解包),无需 .value。
重要特性
- 双向同步:修改 ref 的值会直接修改源对象对应的属性;直接修改源对象属性,ref 也会自动更新。
- 只针对已存在的属性:如果属性不存在,调用时会返回一个 ref,但不会建立响应式连接,修改该 ref 不会影响源对象。
- 可以用于 props 解构:
toRef常用于将props中的某个属性转换为 ref,从而在组合式函数中保持响应性。
这样即使父组件重新传入新的const props = defineProps({ title: String }) const titleRef = toRef(props, 'title')title,titleRef也会同步更新。
toRefs:一次性转换所有属性
基本语法
function toRefs<T extends object>(object: T): {
[K in keyof T]: ToRef<T[K]>
}
toRefs 接收一个响应式对象,返回一个普通对象,该对象的每个属性都是一个与源对象对应属性同步的 ref。
使用示例
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'Vue',
version: 3,
count: 0
})
// 一次性将所有属性转换为 ref
const { name, version, count } = toRefs(state)
function increment() {
count.value++ // 修改 ref,state.count 同步变化
}
</script>
<template>
<p>名称:{{ name }}</p>
<p>版本:{{ version }}</p>
<p>计数:{{ count }}</p>
<button @click="increment">+1</button>
</template>
经典应用场景:从组合式函数中返回响应式数据
当你在一个组合式函数中使用 reactive 管理内部状态时,如果直接返回这个 reactive 对象,使用方解构后就会丢失响应性。toRefs 可以完美解决:
// useMouse.js
import { reactive, onMounted, onUnmounted, toRefs } from 'vue'
export function useMouse() {
const pos = reactive({
x: 0,
y: 0
})
function update(e) {
pos.x = e.pageX
pos.y = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 返回时使用 toRefs,确保解构后依然响应式
return toRefs(pos)
}
在组件中使用:
const { x, y } = useMouse()
// x 和 y 都是 ref,模板中可以直接使用 {{ x }} {{ y }}
注意事项
toRefs只会转换对象自身可枚举属性,不会处理原型链上的属性。- 如果对象本身不是响应式的(普通对象),
toRefs依然会生成 ref,但这些 ref 不会和原对象保持同步(因为普通对象没有响应式依赖)。 toRefs包含对 Symbol 属性的转换。- 当源对象中存在
ref类型的属性时,toRefs不会对该属性进行unref处理,返回的仍然是 ref。
toRef 与 toRefs 的区别总结
| 对比项 | toRef | toRefs |
|---|---|---|
| 转换范围 | 单个属性 | 整个对象的所有属性 |
| 返回值 | 一个 Ref 对象 |
一个普通对象,属性值均为 Ref |
| 适用场景 | 需要单独处理某个属性(如 props 解构) | 需要解构整个 reactive 对象或组合函数返回值 |
| 对新增属性的支持 | 不支持(只能引用已存在的 key) | 返回对象经展开后才获取新的 ref,与源对象新增属性无关 |
两者本质做的事情相同:建立一个 ref 到源对象属性的“读写通道”,保证响应式连接。
搭配 watch 与 computed 的实践
用 toRef 监听某个属性
import { reactive, toRef, watch } from 'vue'
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count')
watch(countRef, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
state.count++ // 触发 watch
用 toRefs 创建计算属性源
import { reactive, toRefs, computed } from 'vue'
const state = reactive({
firstName: 'Zhang',
lastName: 'San'
})
const refs = toRefs(state)
const fullName = computed(() => {
return refs.firstName.value + ' ' + refs.lastName.value
})
常见误区与最佳实践
❌ 误区1:认为 toRef 会创建新的响应式状态
toRef 只是创建一个指向源属性的“引用”,不会拷贝数据。删除该 ref 不会影响原对象,但修改会影响。
❌ 误区2:在 setup 外直接解构 reactive 对象而不使用 toRefs
// 错误
const { name } = state // 丢失响应性
// 正确
const { name } = toRefs(state)
❌ 误区3:在非响应式对象上使用 toRef 期望获得同步
const obj = { a: 1 }
const aRef = toRef(obj, 'a')
aRef.value = 2
console.log(obj.a) // 1,没有同步
toRef 要求第一个参数必须是响应式对象(reactive 创建或响应式对象的属性)。
✅ 最佳实践总结
- 在组合式函数返回数据时,始终使用
toRefs,让使用方可以安全解构。 - 对于
props的某一个属性,需要保持响应式且可能作为参数传递时,使用toRef(props, 'xxx')。 - 单个属性需要独立监听或传递给其他函数时,优先使用
toRef。 - 永远不要直接解构
reactive对象,除非你确定不需要响应性。
完整示例:用户信息编辑表单
<script setup>
import { reactive, toRef, toRefs } from 'vue'
const user = reactive({
username: 'JohnDoe',
email: 'john@example.com',
age: 30
})
// 提取单个属性作为 ref 传递
const usernameRef = toRef(user, 'username')
// 批量提取用于展示
const { email, age } = toRefs(user)
function updateUsername() {
usernameRef.value = 'JaneDoe'
}
</script>
<template>
<div>
<p>用户名:<input v-model="usernameRef" /></p>
<p>邮箱:<input v-model="email" /></p>
<p>年龄:<input v-model.number="age" /></p>
<button @click="updateUsername">重置用户名</button>
<hr />
<p>原始数据:{{ user }}</p>
</div>
</template>