ref / reactive 响应式基础
Vue 3 通过
ref()和reactive()创建响应式数据。ref包装任意值为响应式引用,reactive将对象转为深度响应式代理。二者均基于 ES6 Proxy,替代 Vue 2 的Object.defineProperty。
速查卡片
| API | 入参类型 | 返回值 | 访问方式 | 深度响应式 | 可整体替换 |
|---|---|---|---|---|---|
ref() | 任意类型 | Ref<T> | .value | ✅(对象时) | ✅ |
reactive() | 仅 object | T 的 Proxy | 直接访问属性 | ✅ | ❌ |
shallowRef() | 任意类型 | Ref<T> | .value | ❌(仅 .value 级) | ✅ |
shallowReactive() | 仅 object | T 的 Proxy | 直接访问属性 | ❌(仅根级) | ❌ |
| 场景 | 推荐 |
|---|---|
基本类型(number / string / boolean) | ref() |
| 普通对象 | ref() 或 reactive(),优先 ref() |
| 需要整体替换 | ref() |
| 表单对象 | reactive()(字段分散赋值方便) |
| 大数据量对象、第三方实例 | shallowRef() |
概述
Vue 3 响应式系统基于 ES6 Proxy,解决了 Vue 2 中 Object.defineProperty 的三个致命缺陷:
- ❌ 无法检测对象属性的添加和删除
- ❌ 无法检测数组索引和
length变化 - ❌ 需要
Vue.set/Vue.delete补救
Proxy 直接代理整个对象,所有操作都会被拦截,无需特殊 API。
ref() 详解
基本概念
ref() 接受一个值,返回一个 Ref 对象。值存储在 .value 属性中,读取和修改都通过 .value:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++ // 触发响应式更新
console.log(count.value) // 1为什么需要 .value
ref 是对象包装器,因为在 JavaScript 中基本类型按值传递,无法被追踪。用一个对象包裹后,通过 .value 的 getter/setter 就能拦截读写。
深度响应式
当 ref() 的值是对象时,内部会自动调用 reactive() 做深度转换:
const obj = ref({ nested: { count: 0 } })
obj.value.nested.count = 1 // ✅ 触发更新,深层也被代理如果不需要深度转换(大型对象、第三方实例),用 shallowRef():
import { shallowRef } from 'vue'
const bigData = shallowRef({ /* huge */ })
bigData.value = newData // ✅ 仅 .value 级别触发
bigData.value.inner = 1 // ❌ 不触发模板中自动解包
在模板中访问 ref 时,顶层的 ref 会自动解包,不需要 .value:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<p>{{ count }}</p> <!-- 自动解包,等效 count.value -->
<button @click="count++">+</button>
</template>注意:仅顶层 ref 自动解包。嵌套在对象中的 ref 需要
.value,除非该对象是reactive包装的。
模板引用(Template Refs)
ref 还有一个重要功能:在模板中获取 DOM 节点或子组件实例的引用。
基础用法: 声明一个与模板 ref 属性同名的 ref 变量,挂载后自动绑定:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus() // 直接操作 DOM
})
</script>
<template>
<input ref="inputRef" />
</template>关键时序: 模板 ref 在组件挂载后才被赋值,卸载时重置为 null。因此只能在 onMounted 之后或事件回调中访问。
const el = ref<HTMLElement | null>(null)
console.log(el.value) // null —— 还没挂载
onMounted(() => console.log(el.value)) // <div>...</div>Vue 3.5+ 新方式:useTemplateRef()
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('myInput')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="myInput" />
</template>useTemplateRef 的优势:变量名和模板 ref 属性名可以不同,代码意图更清晰。但 3.5 以下版本只能用同名的 ref 变量。
v-for 中的 ref: 得到的不是单个元素,而是元素数组:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const itemRefs = ref<HTMLLIElement[]>([])
onMounted(() => {
console.log(itemRefs.value.length) // 3
})
</script>
<template>
<ul>
<li v-for="i in 3" :key="i" ref="itemRefs">{{ i }}</li>
</ul>
</template>注意:数组不保证与源数组顺序一致。
组件 ref: ref 作用在子组件上时,获取的是组件实例:
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref<InstanceType<typeof Child> | null>(null)
</script>
<template>
<Child ref="childRef" />
</template>默认只能访问子组件通过 defineExpose 暴露的内容。<script setup> 的组件默认封闭,需显式暴露:
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const reset = () => { count.value = 0 }
defineExpose({ count, reset })
</script>reactive() 详解
基本概念
reactive() 返回对象的 Proxy 代理,直接操作属性即可触发响应:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
state.count++ // ✅ 触发更新,无需 .value深度转换
reactive() 是深度转换,嵌套对象也会被代理:
const state = reactive({ a: { b: { c: 0 } } })
state.a.b.c = 1 // ✅ 全部响应式reactive() 的局限
局限一:仅限对象类型
const n = reactive(0) // ❌ 报错,reactive 不接受基本类型局限二:不能整体替换
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // ❌ 原引用断开,视图不更新!
// 正确做法:用 ref 包裹
const state = ref({ count: 0 })
state.value = { count: 1 } // ✅局限三:解构丢失响应式
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // ❌ 丢失响应式
count++ // 不触发更新!
// 正确做法:用 toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
count.value++ // ✅局限四:不能赋值给 ref 的 .value 后再保持共享引用——返回的是 Proxy,=== 不相等:
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
console.log(reactive(raw) === reactive(raw)) // false(两个独立 Proxy)基础用法
示例一:最简——计数器
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>示例二:典型——表单对象
两种风格均可:
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 风格 A:ref + 对象
const formA = ref({ name: '', age: 0 })
// 风格 B:reactive(字段分散赋值更方便)
const formB = reactive({ name: '', age: 0 })
</script>
<template>
<input v-model="formA.name" />
<input v-model="formB.age" />
</template>示例三:进阶——ref 自动解包 in reactive
import { ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({ count }) // reactive 中的 ref 自动解包
console.log(state.count) // 0,不用 .value
state.count = 5 // ✅ 等价于 count.value = 5
console.log(count.value) // 5
// ⚠️ 但是数组和 Map 不会解包!
const arr = reactive([ref(1), ref(2)])
console.log(arr[0]) // RefImpl { value: 1 },不解包!
console.log(arr[0].value) // 1,需要 .value
const map = reactive(new Map([['key', ref(1)]]))
console.log(map.get('key').value) // 1,Map 也需要 .value示例四:冷门——reactive 代理整个 Map/Set
import { reactive } from 'vue'
// reactive 支持 Map、Set、WeakMap、WeakSet
const map = reactive(new Map<string, number>())
map.set('count', 1)
console.log(map.get('count')) // 1 ✅ 响应式!
const set = reactive(new Set<number>())
set.add(1)
console.log(set.has(1)) // true ✅
// Vue 2 做不到这一点(Object.defineProperty 无法拦截 Map/Set)API 参考
ref()
| 项 | 内容 |
|---|---|
| 签名 | function ref<T>(value: T): Ref<UnwrapRef<T>> |
| 入参 | value: 任意类型的初始值 |
| 返回值 | Ref<T> 对象,含 .value 属性 |
| 深度响应式 | 是(对象值自动调用 reactive()) |
| 版本 | ≥ 3.0.0 |
shallowRef()
| 项 | 内容 |
|---|---|
| 签名 | function shallowRef<T>(value: T): ShallowRef<T> |
| 入参 | value: 任意类型的初始值 |
| 返回值 | ShallowRef<T>,仅 .value 本身响应式 |
| 深度响应式 | 否 |
| 适用场景 | 大型数据、第三方类实例、不需要深度追踪的对象 |
| 版本 | ≥ 3.0.0 |
reactive()
| 项 | 内容 |
|---|---|
| 签名 | function reactive<T extends object>(target: T): UnwrapNestedRefs<T> |
| 入参 | target: 对象类型(普通对象、数组、Map、Set 等) |
| 返回值 | 原始对象的深度响应式 Proxy |
| 限制 | 仅接受对象;不能整体替换;解构丢失响应式 |
| 版本 | ≥ 3.0.0 |
shallowReactive()
| 项 | 内容 |
|---|---|
| 签名 | function shallowReactive<T extends object>(target: T): T |
| 返回值 | 仅根级别属性响应式的 Proxy |
| 版本 | ≥ 3.0.0 |
isRef()
| 项 | 内容 |
|---|---|
| 签名 | function isRef<T>(r: Ref<T> | unknown): r is Ref<T> |
| 说明 | 检查值是否为 ref 对象 |
| 版本 | ≥ 3.0.0 |
unref()
| 项 | 内容 |
|---|---|
| 签名 | function unref<T>(ref: T | Ref<T>): T |
| 说明 | isRef(val) ? val.value : val 的语法糖 |
| 版本 | ≥ 3.0.0 |
toRef() / toRefs()
| 项 | 内容 |
|---|---|
toRef(obj, key) | 为 reactive 对象的某个属性创建独立 ref,保持双向绑定 |
toRefs(obj) | 将 reactive 对象所有属性转为 ref 集合,用于解构 |
| 版本 | ≥ 3.0.0 |
triggerRef()
| 项 | 内容 |
|---|---|
| 签名 | function triggerRef(ref: ShallowRef): void |
| 说明 | 强制触发 shallowRef 的依赖更新(当直接修改深层属性时) |
| 版本 | ≥ 3.0.0 |
useTemplateRef()
| 项 | 内容 |
|---|---|
| 签名 | function useTemplateRef<T>(key: string): Readonly<ShallowRef<T | null>> |
| 说明 | 3.5+ 新增,按名称获取模板 ref,替代同名变量约定 |
| 版本 | ≥ 3.5.0 |
类型定义
// ref 核心类型
interface Ref<T> {
value: T
}
function ref<T>(value: T): Ref<UnwrapRef<T>>
function ref<T = any>(): Ref<T | undefined>
// reactive 核心类型
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// shallowRef
interface ShallowRef<T> {
value: T
}
function shallowRef<T>(value: T): ShallowRef<T>
// 类型工具
type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRef<V> : T
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
// isRef / unref
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
function unref<T>(ref: T | Ref<T>): Tref vs reactive 决策指南
| 场景 | ref | reactive | 说明 |
|---|---|---|---|
| 基本类型(string / number / boolean) | ✅ | ❌ | reactive 不接受基本类型 |
| 模板 DOM 引用 | ✅ | ❌ | ref="xxx" 必须绑定 ref 变量 |
| 组件实例引用 | ✅ | ❌ | 同上,配合 defineExpose |
| 需要整体替换对象 | ✅ | ❌ | state.value = newObj |
| 表单对象(字段分散修改) | ✅ | ✅ | reactive 不用 .value 略便捷 |
| 解构使用 | ✅ (toRefs) | ⚠️ (需 toRefs) | 都需辅助 |
| Map / Set / WeakMap / WeakSet | — | ✅ | reactive 原生支持 |
| Props 传递 | ✅ | ✅ (3.5+ 解构) | 3.5 起支持响应式 Props 解构 |
| 组合函数返回值 | ✅ | ❌ | 返回 reactive 解构会丢失 |
| 第三方类实例 | ✅ (shallowRef) | ❌ | 避免深度代理导致异常 |
| 大数据量(千级以上属性) | ✅ (shallowRef) | ⚠️ | 深度代理有性能开销 |
社区共识:Vue 核心团队和多数开发者倾向于优先使用
ref,它在灵活性上更有优势,代码一致性更好。reactive在特定场景(Map/Set、表单对象)仍是正确选择。
注意事项与边界情况
ref 解包的陷阱
// ❌ reactive 数组不解包 ref
const arr = reactive([ref(1)])
console.log(arr[0]) // RefImpl,不是 1
console.log(arr[0].value) // 1
// ❌ reactive Map 不解包 ref
const map = reactive(new Map([['k', ref(1)]]))
map.get('k') // RefImpl
map.get('k').value // 1
// ✅ 对象属性自动解包
const obj = reactive({ x: ref(1) })
obj.x // 1,自动解包!顶层 ref 在模板中解包的例外
<!-- ✅ 顶层 ref 自动解包 -->
<template>{{ count }}</template>
<!-- ❌ 解构后不再是顶层 -->
<template>{{ { count }.count }}</template> <!-- 显示 RefImpl -->reactive 的 Proxy 身份
const raw = { x: 1 }
const proxy = reactive(raw)
console.log(raw === proxy) // false
console.log(reactive(raw) === reactive(raw)) // false —— 每次都返回独立 Proxy
// 最佳实践:始终用同一变量持有 Proxy
const state = reactive({ count: 0 })
// 之后只用 state,不要 reactive(raw) 再创建新的对象作为 ref 值的身份丢失
class MyClass {
private secret = 'hidden'
}
const instance = ref(new MyClass())
// instance.value 不再是 MyClass 实例——内部 Proxy 导致 private 字段不可访问
// 使用 shallowRef 可以保留原始身份
const safe = shallowRef(new MyClass())
safe.value // ✅ 仍是 MyClass 实例triggerRef——强制 shallowRef 更新
const state = shallowRef({ list: [] })
state.value.list.push(1) // ❌ 不触发更新(深层变更)
// 解决:改引用 或 手动触发
state.value = { ...state.value, list: [...state.value.list, 1] } // 方案 A
triggerRef(state) // 方案 B:手动触发(需要 import { triggerRef })常见误区
❌
ref(0)返回的是数字 → 返回的是Ref<number>对象,模板中自动解包,JS 中需.value。❌ 解构
reactive后仍响应式 → 解构就是取出原始值,必须用toRefs()包裹。❌
reactive比ref"更高级" → 二者只是不同场景的工具。ref更通用,社区倾向优先使用ref。❌
ref()包装对象时不是深度响应式 → 是深度响应式!内部会自动调用reactive()。❌ 数组中
ref会自动解包 → 不会。数组和 Map/Set 都不会解包 ref,需要.value。❌
reactive(reactive(obj))返回同一个 Proxy → 不!每次reactive()调用都返回独立 Proxy,判断相等会出问题。❌
watch监听ref需要.value→ 不需要,watch(ref, callback)自动追踪.value(但reactive属性需 getter 函数)。❌
computed返回的值可以直接赋值 → 不能,computed默认只读。声明get+set才可写。❌ 在
setup顶层直接访问模板 ref → 挂载前ref.value为null,必须在onMounted或事件回调中访问。
版本信息
| 版本 | 变更 |
|---|---|
| 3.0.0 | ref() / reactive() / shallowRef() / shallowReactive() / isRef() / unref() / toRef() / toRefs() / triggerRef() 首次发布 |
| 3.2.0 | triggerRef() 增强 |
| 3.3.0 | toRef() 和 toValue() 增强;UnwrapRef 类型改进 |
| 3.4.0 | 响应式系统重大重构(性能提升、更精确的依赖追踪) |
| 3.5.0 | 响应式 Props 解构稳定 |
来源
| 来源 | 链接 |
|---|---|
| 官方指南 — Reactivity Fundamentals | vuejs.org/guide/essentials/reactivity-fundamentals |
| 官方 API — Reactivity Core | vuejs.org/api/reactivity-core |
| 官方指南 — Reactivity in Depth | vuejs.org/guide/extras/reactivity-in-depth |
| GitHub Issues — 常见响应式问题 | vuejs/docs#849 |
| GitHub PR — toRef 边界修复 | vuejs/core#12420 |
最后整理:2026-07-05 · 对应 Vue 版本:3.5.x