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 的变化

解构后得到的是原始值的拷贝,和源对象失去了响应式连接。这会导致视图无法更新,逻辑难以维护。

toReftoRefs 正是为了解决这个问题。


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')
    
    这样即使父组件重新传入新的 titletitleRef 也会同步更新。

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>