Skip to content

toRef / toRefs — 解构保持响应式

reactive() 对象的属性转为独立 ref,保持与源对象的双向同步,使解构后的变量不丢失响应式连接。

速查卡片

toReftoRefs
签名toRef(obj, key) / toRef(getter) / toRef(value)toRefs(obj)
返回类型Ref<T>Readonly<Ref<T>>{ [K in keyof T]: Ref<T[K]> }
双向绑定✅(对象属性模式)
不存在属性仍创建 ref(值为 undefined)不创建(只遍历可枚举属性)
接受类型reactive 对象 / ref / getter / 普通值reactive 对象 / 含 ref 的普通对象
3.3 增强getter 规范化
最低版本3.0.0(规范化模式 3.3.0+)3.0.0

概述

问题背景

reactive() 返回的 Proxy 对象在解构赋值时会丢失响应式——解构取出的是原始值的快照,不再是响应式代理:

typescript
const state = reactive({ count: 0, name: 'Vue' })

// ❌ 解构后 count 是普通数字,修改不触发更新
const { count, name } = state
count++ // 视图无变化

toReftoRefs 解决这个问题:它们在源对象属性和 ref 之间建立引用通道,读写 .value 时直接操作源属性,保持双向同步。

适用场景

场景推荐方案
从 composable 返回响应式状态toRefs(state) 包裹后 return,调用方可安全解构
传递单个响应式属性给子组件 / composabletoRef(props, 'key')toRef(() => props.key)
解构 reactive 后需要局部修改toRefs(state) 或单独 toRef(state, 'key')
composable 接受可能为 getter 的参数toRef(getter) 规范化输入(3.3+)

不适用场景

  • 使用 ref() 定义的数据 → 本身就能解构(.value 是 ref 的一部分,不丢失)
  • 简单值传递,不需要双向绑定 → 直接用 ref(value) 创建独立副本

toRef() —— 单个属性解构

语法签名

typescript
// 1. 规范化签名(3.3+):接受值 / ref / getter
function toRef<T>(value: T):
  T extends () => infer R
    ? Readonly<Ref<R>>       // getter → 只读 ref
    : T extends Ref
      ? T                     // 已是 ref → 原样返回
      : Ref<UnwrapRef<T>>     // 普通值 → 新建 ref

// 2. 对象属性签名:从 reactive 对象取出单个属性
function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]>

type ToRef<T> = T extends Ref ? T : Ref<T>

参数详解

参数类型必填说明
object / valueobject | Ref | (() => T) | T源对象、ref、getter 函数或普通值
keystring | number | symbol属性模式必填源对象上的属性名
defaultValueT[K]否(3.3+)属性不存在时的默认值

返回值行为

输入返回行为
toRef(reactiveObj, 'key')ObjectRefImpl双向绑定,读写 .value 即读写源属性
toRef(reactiveObj, 'nonexistent')ObjectRefImpl(值为 undefined)ref 仍被创建,后续源对象上新增同名属性不会自动同步到这个 ref
toRef(reactiveObj, 'key', defaultValue)ObjectRefImpl(值为默认值)3.3+,属性不存在时使用默认值
toRef(existingRef)同一个 ref直接返回,无额外包装
toRef(() => expr)GetterRefImpl(只读)3.3+,每次 .value 访问都重新执行 getter,不缓存
toRef(plainValue)Ref<UnwrapRef<T>>3.3+ 规范化模式,等价于 ref(plainValue)

基础用法

示例一:最简用法

vue
<script setup lang="ts">
import { reactive, toRef } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })

// 为单个属性创建独立 ref,保持双向绑定
const countRef = toRef(state, 'count')

function increment() {
  countRef.value++ // 修改 ref 同时更新 state.count
}
</script>

<template>
  <p>count: {{ countRef }}</p>
  <p>state.count: {{ state.count }}</p>
  <button @click="increment">+1</button>
</template>

示例二:典型场景 —— 传递 prop 给 composable

typescript
import { toRef } from 'vue'
import type { Ref } from 'vue'

// composable 期望接收 Ref<number>
function useCounter(count: Ref<number>) {
  function increment() { count.value++ }
  return { increment }
}

// 组件中使用
const props = defineProps<{ initialCount: number }>()
const { increment } = useCounter(toRef(props, 'initialCount'))

示例三:进阶 —— 3.3+ getter 规范化

typescript
import { toRef, watchEffect } from 'vue'

const props = defineProps<{ user: { name: string } }>()

// 3.3+ 新写法:比 computed(() => props.user.name) 更简洁
const name = toRef(() => props.user.name)

watchEffect(() => {
  console.log('name changed:', name.value)
})

示例四:冷门 —— 不存在的属性 + 默认值

typescript
import { reactive, toRef } from 'vue'

const state = reactive<{ count?: number }>({ count: 1 })

// 为可能不存在的属性创建 ref,提供 fallback
const total = toRef(state, 'count', 0)
// 如果 state.count 为 undefined,total.value === 0

示例五:冷门 —— 对已有 ref 调用 toRef

typescript
import { ref, toRef } from 'vue'

const existing = ref(42)
const wrapped = toRef(existing)

console.log(wrapped === existing) // true —— 原样返回,不二次包装

toRefs() —— 批量解构

语法签名

typescript
function toRefs<T extends object>(object: T): ToRefs<T>

type ToRefs<T = any> = {
  [K in keyof T]: ToRef<T[K]>
}

参数详解

参数类型必填说明
objectobject响应式对象。非 reactive 对象在 dev 环境会触发警告

返回值

返回一个普通对象(非 Proxy),其每个可枚举属性都是指向源对象对应属性的 Ref。Symbol 属性不会被转换。

基础用法

示例一:最简用法

vue
<script setup lang="ts">
import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })

// toRefs 将所有属性转为 ref
const { count, name } = toRefs(state)

// 现在可以通过 .value 读写,并保持与 state 同步
function increment() {
  count.value++
}
</script>

<template>
  <p>{{ count }} / {{ name }}</p>
</template>

示例二:典型场景 —— composable 返回响应式状态

typescript
// useMouse.ts
import { reactive, toRefs } from 'vue'

export function useMouse() {
  const state = reactive({ x: 0, y: 0 })

  function update(e: MouseEvent) {
    state.x = e.pageX
    state.y = e.pageY
  }

  // 返回 toRefs 后的对象,调用方可安全解构
  return {
    ...toRefs(state),
    update
  }
}
vue
<script setup lang="ts">
import { useMouse } from './useMouse'

// ✅ 解构后 x, y 仍然是响应式的
const { x, y, update } = useMouse()
</script>

示例三:进阶 —— toRefs 作用于 ref 包裹的响应式对象

typescript
import { ref, toRefs } from 'vue'

const objRef = ref({ a: 1, b: 2 })

// toRefs 会展开 ref.value
const { a, b } = toRefs(objRef)

// a.value 和 b.value 现在指向 objRef.value 的属性
// 但注意:这是对当前 ref.value 的快照展开
// 如果 objRef.value 被整体替换,a 和 b 不会更新指向新对象的属性

示例四:边界 —— 对普通对象调用 toRefs

typescript
import { toRefs } from 'vue'

const plain = { x: 1, y: 2 }
const refs = toRefs(plain)
// ⚠️ DEV 环境警告:toRefs() expects a reactive object but received a plain one.
// refs.x, refs.y 只是普通 ref(值为 1, 2),与源对象无连接

示例五:冷门 —— 数组的 toRefs

typescript
import { reactive, toRefs } from 'vue'

const list = reactive([10, 20, 30])
const listRefs = toRefs(list)

// listRefs 是一个对象,属性名为数组索引字符串
console.log(listRefs[0].value) // 10 —— 需要通过字符串索引访问

对比与选择

toRef vs toRefs

维度toReftoRefs
粒度单个属性全部可枚举属性
适用传单个属性给子组件/composablecomposable 返回值、模板解构
不存在属性仍创建 ref不创建
返回值类型Ref<T>{ [K in keyof T]: Ref<T[K]> }

toRef(getter) vs computed

维度toRef(() => expr)computed(() => expr)
返回值Readonly<Ref<T>>ComputedRef<T>
缓存不缓存,每次 .value 都重新计算缓存,依赖不变不重新计算
触发时机每次访问 .value 都重新执行 getter仅依赖变化时重新计算
适用场景简单属性访问(如 () => props.foo有计算成本的派生值
版本3.3.0+3.0.0+

注意事项

  1. toRef 对不存在属性的行为toRef(state, 'nonexistent') 会创建一个值为 undefined 的 ref。但如果源对象后续动态新增了同名属性,这个 ref 不会自动同步到新值——因为它绑定的是属性访问路径,而该路径原本就不存在属性。

  2. toRefs 只处理可枚举属性:Symbol 属性、不可枚举属性不会被转换。需要为这些属性创建 ref 时,手动使用 toRef()

  3. toRefs 对非 reactive 对象:在开发环境下会发出警告,返回的 ref 与源对象没有连接——修改 ref.value 不会更新源对象。

  4. getter 模式不缓存toRef(() => expr) 内部使用 GetterRefImpl,每次 .value 访问都重新求值。如果需要缓存行为(依赖不变不重新计算),使用 computed()

  5. Props 只读限制toRef(props, 'key') 创建的 ref 仍然遵守 props 只读约束。尝试 ref.value = newVal 在开发环境会触发警告,生产环境静默失败。

  6. 数组解构陷阱toRefs(reactiveArray) 返回以字符串索引为键的对象,不能用数组解构 const [a, b] = ...。需要逐个通过索引访问:arrayRefs[0].value

常见误区

❌ 误区 1:混淆 toRef 和 ref

typescript
const state = reactive({ count: 1 })

// ❌ 错误:ref(state.count) 取的是当前快照值 1,断开连接
const count = ref(state.count)

// ✅ 正确:toRef 保持双向绑定
const count = toRef(state, 'count')

❌ 误区 2:认为 toRefs 后可以整体替换

typescript
const stateAsRefs = toRefs(state)

// ❌ 错误:stateAsRefs 是普通对象,不是响应式的
stateAsRefs = toRefs(newState) // 不会触发任何更新

// ✅ 正确:通过 .value 修改每个属性,或者重新执行 toRefs 后整体替换外层 ref

❌ 误区 3:toRef(getter) 和 computed 行为相同

typescript
const obj = ref({ id: 'foo' })

// toRef getter 模式每次 .value 都会重新执行
const id1 = toRef(() => obj.value.id)

// computed 只在 obj.value.id 实际变化时才触发
const id2 = computed(() => obj.value.id)

// 即使 obj.value 被替换为 { id: 'foo' }(值相同),
// watchEffect(() => id1.value) 仍会触发(不缓存)
// watchEffect(() => id2.value) 不会触发(computed 缓存)

❌ 误区 4:toRefs 可以用于 props 解构

typescript
const props = defineProps<{ count: number }>()

// ❌ 错误:props 不是 reactive() 创建的,而是 shallowReactive
// toRefs(props) 创建的 ref 与 props 只有浅层同步
const { count } = toRefs(props)

// ✅ 正确:用 toRef
const count = toRef(props, 'count')

版本信息

版本变更
3.0.0toRef() / toRefs() 首次发布(仅对象属性模式)
3.3.0toRef() 增加规范化签名,支持 getter / ref / 普通值规范化;增加 defaultValue 参数;toValue() 新增
3.5.0toRef 边界情况修复(ref unwrapping 边缘 case,见 PR#12420

来源

来源链接
官方文档 — 响应式工具 APIhttps://cn.vuejs.org/api/reactivity-utilities.html
源码 — ref.tspackages/reactivity/src/ref.ts
类型声明@vue/reactivity@3.5.39 reactivity.d.ts:549-610
3.3 发布博客https://blog.vuejs.org/posts/vue-3-3
Issue: toRef vs computed getter 行为差异https://github.com/vuejs/core/issues/13819