toRef / toRefs — 解构保持响应式
将
reactive()对象的属性转为独立ref,保持与源对象的双向同步,使解构后的变量不丢失响应式连接。
速查卡片
| 项 | toRef | toRefs |
|---|---|---|
| 签名 | 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 对象在解构赋值时会丢失响应式——解构取出的是原始值的快照,不再是响应式代理:
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 解构后 count 是普通数字,修改不触发更新
const { count, name } = state
count++ // 视图无变化toRef 和 toRefs 解决这个问题:它们在源对象属性和 ref 之间建立引用通道,读写 .value 时直接操作源属性,保持双向同步。
适用场景
| 场景 | 推荐方案 |
|---|---|
| 从 composable 返回响应式状态 | toRefs(state) 包裹后 return,调用方可安全解构 |
| 传递单个响应式属性给子组件 / composable | toRef(props, 'key') 或 toRef(() => props.key) |
| 解构 reactive 后需要局部修改 | toRefs(state) 或单独 toRef(state, 'key') |
| composable 接受可能为 getter 的参数 | toRef(getter) 规范化输入(3.3+) |
不适用场景
- 使用
ref()定义的数据 → 本身就能解构(.value是 ref 的一部分,不丢失) - 简单值传递,不需要双向绑定 → 直接用
ref(value)创建独立副本
toRef() —— 单个属性解构
语法签名
// 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 / value | object | Ref | (() => T) | T | 是 | 源对象、ref、getter 函数或普通值 |
key | string | number | symbol | 属性模式必填 | 源对象上的属性名 |
defaultValue | T[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) |
基础用法
示例一:最简用法
<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
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 规范化
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)
})示例四:冷门 —— 不存在的属性 + 默认值
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
import { ref, toRef } from 'vue'
const existing = ref(42)
const wrapped = toRef(existing)
console.log(wrapped === existing) // true —— 原样返回,不二次包装toRefs() —— 批量解构
语法签名
function toRefs<T extends object>(object: T): ToRefs<T>
type ToRefs<T = any> = {
[K in keyof T]: ToRef<T[K]>
}参数详解
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
object | object | 是 | 响应式对象。非 reactive 对象在 dev 环境会触发警告 |
返回值
返回一个普通对象(非 Proxy),其每个可枚举属性都是指向源对象对应属性的 Ref。Symbol 属性不会被转换。
基础用法
示例一:最简用法
<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 返回响应式状态
// 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
}
}<script setup lang="ts">
import { useMouse } from './useMouse'
// ✅ 解构后 x, y 仍然是响应式的
const { x, y, update } = useMouse()
</script>示例三:进阶 —— toRefs 作用于 ref 包裹的响应式对象
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
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
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
| 维度 | toRef | toRefs |
|---|---|---|
| 粒度 | 单个属性 | 全部可枚举属性 |
| 适用 | 传单个属性给子组件/composable | composable 返回值、模板解构 |
| 不存在属性 | 仍创建 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+ |
注意事项
toRef 对不存在属性的行为:
toRef(state, 'nonexistent')会创建一个值为undefined的 ref。但如果源对象后续动态新增了同名属性,这个 ref 不会自动同步到新值——因为它绑定的是属性访问路径,而该路径原本就不存在属性。toRefs 只处理可枚举属性:Symbol 属性、不可枚举属性不会被转换。需要为这些属性创建 ref 时,手动使用
toRef()。toRefs 对非 reactive 对象:在开发环境下会发出警告,返回的 ref 与源对象没有连接——修改
ref.value不会更新源对象。getter 模式不缓存:
toRef(() => expr)内部使用GetterRefImpl,每次.value访问都重新求值。如果需要缓存行为(依赖不变不重新计算),使用computed()。Props 只读限制:
toRef(props, 'key')创建的 ref 仍然遵守 props 只读约束。尝试ref.value = newVal在开发环境会触发警告,生产环境静默失败。数组解构陷阱:
toRefs(reactiveArray)返回以字符串索引为键的对象,不能用数组解构const [a, b] = ...。需要逐个通过索引访问:arrayRefs[0].value。
常见误区
❌ 误区 1:混淆 toRef 和 ref
const state = reactive({ count: 1 })
// ❌ 错误:ref(state.count) 取的是当前快照值 1,断开连接
const count = ref(state.count)
// ✅ 正确:toRef 保持双向绑定
const count = toRef(state, 'count')❌ 误区 2:认为 toRefs 后可以整体替换
const stateAsRefs = toRefs(state)
// ❌ 错误:stateAsRefs 是普通对象,不是响应式的
stateAsRefs = toRefs(newState) // 不会触发任何更新
// ✅ 正确:通过 .value 修改每个属性,或者重新执行 toRefs 后整体替换外层 ref❌ 误区 3:toRef(getter) 和 computed 行为相同
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 解构
const props = defineProps<{ count: number }>()
// ❌ 错误:props 不是 reactive() 创建的,而是 shallowReactive
// toRefs(props) 创建的 ref 与 props 只有浅层同步
const { count } = toRefs(props)
// ✅ 正确:用 toRef
const count = toRef(props, 'count')版本信息
| 版本 | 变更 |
|---|---|
| 3.0.0 | toRef() / toRefs() 首次发布(仅对象属性模式) |
| 3.3.0 | toRef() 增加规范化签名,支持 getter / ref / 普通值规范化;增加 defaultValue 参数;toValue() 新增 |
| 3.5.0 | toRef 边界情况修复(ref unwrapping 边缘 case,见 PR#12420) |
来源
| 来源 | 链接 |
|---|---|
| 官方文档 — 响应式工具 API | https://cn.vuejs.org/api/reactivity-utilities.html |
| 源码 — ref.ts | packages/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 |