Skip to content

ref / reactive 响应式基础

Vue 3 通过 ref()reactive() 创建响应式数据。ref 包装任意值为响应式引用,reactive 将对象转为深度响应式代理。二者均基于 ES6 Proxy,替代 Vue 2 的 Object.defineProperty

速查卡片

API入参类型返回值访问方式深度响应式可整体替换
ref()任意类型Ref<T>.value✅(对象时)
reactive()objectT 的 Proxy直接访问属性
shallowRef()任意类型Ref<T>.value❌(仅 .value 级)
shallowReactive()objectT 的 Proxy直接访问属性❌(仅根级)
场景推荐
基本类型(number / string / booleanref()
普通对象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

typescript
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() 做深度转换:

typescript
const obj = ref({ nested: { count: 0 } })
obj.value.nested.count = 1 // ✅ 触发更新,深层也被代理

如果不需要深度转换(大型对象、第三方实例),用 shallowRef()

typescript
import { shallowRef } from 'vue'
const bigData = shallowRef({ /* huge */ })
bigData.value = newData  // ✅ 仅 .value 级别触发
bigData.value.inner = 1  // ❌ 不触发

模板中自动解包

在模板中访问 ref 时,顶层的 ref 会自动解包,不需要 .value

vue
<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 变量,挂载后自动绑定:

vue
<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 之后或事件回调中访问。

ts
const el = ref<HTMLElement | null>(null)
console.log(el.value) // null —— 还没挂载
onMounted(() => console.log(el.value)) // <div>...</div>

Vue 3.5+ 新方式:useTemplateRef()

vue
<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: 得到的不是单个元素,而是元素数组:

vue
<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 作用在子组件上时,获取的是组件实例:

vue
<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> 的组件默认封闭,需显式暴露:

vue
<!-- 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 代理,直接操作属性即可触发响应:

typescript
import { reactive } from 'vue'

const state = reactive({ count: 0 })
state.count++ // ✅ 触发更新,无需 .value

深度转换

reactive() 是深度转换,嵌套对象也会被代理:

typescript
const state = reactive({ a: { b: { c: 0 } } })
state.a.b.c = 1 // ✅ 全部响应式

reactive() 的局限

局限一:仅限对象类型

typescript
const n = reactive(0) // ❌ 报错,reactive 不接受基本类型

局限二:不能整体替换

typescript
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // ❌ 原引用断开,视图不更新!
// 正确做法:用 ref 包裹
const state = ref({ count: 0 })
state.value = { count: 1 } // ✅

局限三:解构丢失响应式

typescript
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,=== 不相等:

typescript
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
console.log(reactive(raw) === reactive(raw)) // false(两个独立 Proxy)

基础用法

示例一:最简——计数器

vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

示例二:典型——表单对象

两种风格均可:

vue
<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

typescript
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

typescript
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

类型定义

typescript
// 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>): T

ref vs reactive 决策指南

场景refreactive说明
基本类型(string / number / boolean)reactive 不接受基本类型
模板 DOM 引用ref="xxx" 必须绑定 ref 变量
组件实例引用同上,配合 defineExpose
需要整体替换对象state.value = newObj
表单对象(字段分散修改)reactive 不用 .value 略便捷
解构使用✅ (toRefs)⚠️ (需 toRefs)都需辅助
Map / Set / WeakMap / WeakSetreactive 原生支持
Props 传递✅ (3.5+ 解构)3.5 起支持响应式 Props 解构
组合函数返回值返回 reactive 解构会丢失
第三方类实例✅ (shallowRef)避免深度代理导致异常
大数据量(千级以上属性)✅ (shallowRef)⚠️深度代理有性能开销

社区共识:Vue 核心团队和多数开发者倾向于优先使用 ref,它在灵活性上更有优势,代码一致性更好。reactive 在特定场景(Map/Set、表单对象)仍是正确选择。

注意事项与边界情况

ref 解包的陷阱

typescript
// ❌ 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 在模板中解包的例外

vue
<!-- ✅ 顶层 ref 自动解包 -->
<template>{{ count }}</template>

<!-- ❌ 解构后不再是顶层 -->
<template>{{ { count }.count }}</template>  <!-- 显示 RefImpl -->

reactive 的 Proxy 身份

typescript
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 值的身份丢失

typescript
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 更新

typescript
const state = shallowRef({ list: [] })
state.value.list.push(1) // ❌ 不触发更新(深层变更)

// 解决:改引用 或 手动触发
state.value = { ...state.value, list: [...state.value.list, 1] } // 方案 A
triggerRef(state) // 方案 B:手动触发(需要 import { triggerRef })

常见误区

  1. ref(0) 返回的是数字 → 返回的是 Ref<number> 对象,模板中自动解包,JS 中需 .value

  2. 解构 reactive 后仍响应式 → 解构就是取出原始值,必须用 toRefs() 包裹。

  3. reactiveref "更高级" → 二者只是不同场景的工具。ref 更通用,社区倾向优先使用 ref

  4. ref() 包装对象时不是深度响应式 → 是深度响应式!内部会自动调用 reactive()

  5. 数组中 ref 会自动解包 → 不会。数组和 Map/Set 都不会解包 ref,需要 .value

  6. reactive(reactive(obj)) 返回同一个 Proxy → 不!每次 reactive() 调用都返回独立 Proxy,判断相等会出问题。

  7. watch 监听 ref 需要 .value → 不需要,watch(ref, callback) 自动追踪 .value(但 reactive 属性需 getter 函数)。

  8. computed 返回的值可以直接赋值 → 不能,computed 默认只读。声明 get + set 才可写。

  9. setup 顶层直接访问模板 ref → 挂载前 ref.valuenull,必须在 onMounted 或事件回调中访问。

版本信息

版本变更
3.0.0ref() / reactive() / shallowRef() / shallowReactive() / isRef() / unref() / toRef() / toRefs() / triggerRef() 首次发布
3.2.0triggerRef() 增强
3.3.0toRef()toValue() 增强;UnwrapRef 类型改进
3.4.0响应式系统重大重构(性能提升、更精确的依赖追踪)
3.5.0响应式 Props 解构稳定

来源

来源链接
官方指南 — Reactivity Fundamentalsvuejs.org/guide/essentials/reactivity-fundamentals
官方 API — Reactivity Corevuejs.org/api/reactivity-core
官方指南 — Reactivity in Depthvuejs.org/guide/extras/reactivity-in-depth
GitHub Issues — 常见响应式问题vuejs/docs#849
GitHub PR — toRef 边界修复vuejs/core#12420

最后整理:2026-07-05 · 对应 Vue 版本:3.5.x