Vue.js: 响应式数据:ref/reactive
最后更新:2026-08-26
Vue 3 的响应式系统是它的灵魂——当你修改一个变量,页面上所有用到它的地方自动更新。这就是为什么 Vue 写起来比 jQuery 简单 10 倍。响应式系统由两个核心 API 支撑:ref() 和 reactive()。
ref 用于包装任何类型的数据(数字、字符串、对象),reactive 只能包装对象。两者用法不同,本课帮你彻底搞懂。
1. 你将学到
- 响应式数据的本质:Proxy 拦截 + 依赖收集 + 触发更新
ref()包装任何值,.value访问的语法reactive()包装对象,自动深度响应- ref vs reactive 的 5 大区别和选择场景
- shallowRef / shallowReactive 浅层响应
- toRef / toRefs 工具函数的用法
- 响应式失效的 5 个常见陷阱
2. 一个计数器页面的"翻车"故事
(1) 痛点:改 count,UI 不更新
Alice 写了她的第一个 Vue 3 组件用于电商后台。她尝试实现一个"商品库存计数器":
JS
// ❌ 翻车版
let count = 0
function increment() {
count++
console.log(count) // 1, 2, 3... 看起来对
}
HTML
<template>
<p>Stock: {{ count }}</p>
<button @click="increment">+1</button>
</template>
她点击按钮 5 次。控制台显示 1, 2, 3, 4, 5。但页面仍然显示 "Stock: 0"。
产品经理 Charlie 在她身后看了一眼:
"Alice,count 怎么不更新?我点了 5 次。Vue 坏了?"
(2) Vue 响应式解法:ref() / reactive()
VUE
<!-- ✅ 正确版 -->
<template>
<p>Stock: {{ count }}</p>
<button @click="increment">+1</button>
</template>
<script setup>
import { ref } from 'vue'
// 用 ref() 包装 → Vue 知道它要"响应式"
const count = ref(0)
function increment() {
// 在 JS 中要 .value
count.value++
// 模板中自动解包,不需要 .value
}
</script>
现在点击按钮,页面瞬间更新。变量变化 → DOM 自动更新。
(3) 收益
学会 Vue 3 响应式后:
- 代码量:30 行 jQuery DOM 操作 → 5 行 Vue
- Bug 率:50%(忘记更新 DOM)→ 0%(Vue 自动处理)
- 心智负担:"哪些 DOM 元素依赖这个变量?" → "全部,自动的"
3. 响应式原理:Proxy + 依赖收集
(1) 3 步响应流程
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant D as Data
T->>C: 1. Render (读取 count)
C->>R: 2. get count
R->>D: 3. Read 0
R->>R: 4. Track: 谁在用 count?
R-->>C: 5. Return 0
C-->>T: 6. Display "Stock: 0"
Note over D,T: 用户点击按钮
D->>R: 7. set count = 1
R->>R: 8. Trigger: 通知所有用 count 的地方
R->>C: 9. Re-render
C-->>T: 10. Display "Stock: 1"
(2) 关键点
- Proxy 拦截:Vue 3 用 ES6 Proxy 拦截 get/set 操作
- 依赖收集:模板渲染时记录"谁用了这个数据"(track)
- 触发更新:数据变化时通知"所有用到的地方"重新渲染(trigger)
(3) Vue 2 vs Vue 3 响应式对比
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| 底层 API | Object.defineProperty |
ES6 Proxy |
| 数组监听 | 需特殊处理 | 原生支持 |
| Map/Set 监听 | 不支持 | 支持 |
| 添加新属性 | 需 Vue.set() |
自动响应 |
| 性能 | 一般 | 更好(懒代理) |
4. ref():包装任何值
(1) 基本用法
JS
import { ref } from 'vue'
// 包装数字
const count = ref(0)
// 包装字符串
const name = ref('Alice')
// 包装布尔值
const isVip = ref(false)
// 包装 null
const data = ref(null)
// 包装数组
const items = ref([1, 2, 3])
// 包装对象
const user = ref({ name: 'Bob', age: 25 })
(2) 访问值:JS 用 .value,模板自动解包
VUE
<template>
<!-- 模板中直接用变量名(自动解包) -->
<p>{{ count }}</p> <!-- 0 -->
<p>{{ name }}</p> <!-- Alice -->
<p>{{ user.name }}</p> <!-- Bob -->
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const name = ref('Alice')
const user = ref({ name: 'Bob', age: 25 })
function logCount() {
// 在 JS 中必须用 .value
console.log(count.value) // 0
console.log(name.value) // Alice
console.log(user.value.name) // Bob
}
</script>
(3) ref 的"双向"特性
JS
import { ref } from 'vue'
const count = ref(0)
// 读取
console.log(count.value) // 0
// 修改
count.value = 1
console.log(count.value) // 1
// 修改嵌套属性(ref 包装对象时)
const user = ref({ name: 'Bob' })
user.value.name = 'Alice' // ✅ 响应式
user.value = { name: 'Charlie' } // ✅ 整个替换也响应
5. reactive():包装对象(深度响应)
(1) 基本用法
JS
import { reactive } from 'vue'
// 只能传对象
const state = reactive({
count: 0,
user: { name: 'Alice', age: 25 },
items: [1, 2, 3]
})
// 访问不需要 .value
console.log(state.count) // 0
console.log(state.user.name) // Alice
// 修改自动响应
state.count = 1
state.user.name = 'Bob'
state.items.push(4)
(2) 深度响应
JS
const state = reactive({
level1: {
level2: {
level3: {
value: 'deep'
}
}
}
})
// 修改任何层级都自动响应
state.level1.level2.level3.value = 'updated' // ✅ 触发更新
(3) reactive 的限制
JS
// ❌ 不能直接替换整个对象(失去响应式)
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 错!state 变量本身变了,但模板里用的是旧引用
// ❌ 解构会丢失响应式
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = state // count/name 变成普通变量,不响应了
// ✅ 解构保持响应式:用 toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state) // count/name.value 保持响应
6. ref vs reactive 5 大区别
| 维度 | ref | reactive |
|---|---|---|
| 可包装类型 | 任何值(数字、字符串、对象、数组) | 仅对象(Object/Array) |
| 访问方式 | .value(JS) |
直接访问(JS) |
| 模板访问 | 自动解包(无 .value) | 直接访问 |
| 整体替换 | ref.value = {...} ✅ 响应 |
reactive({...}) = ... ❌ 不响应 |
| 解构 | 保持响应(toRefs) | 丢失响应(需 toRefs) |
(4) 选择建议
| 场景 | 推荐 |
|---|---|
| 基本类型(number/string/boolean) | ref() |
| 单个响应式对象 | ref({}) 或 reactive({}) 都可 |
| 多个相关字段组成 state | reactive({})(像 Vuex) |
| 需要整体替换 | ref({})(更灵活) |
| 传 props / emits 用 | ref()(明确边界) |
7. 浅层响应:shallowRef / shallowReactive
(1) 什么时候用浅层响应?
当你有一个大对象,但只关心顶层字段变化时,浅层响应能节省性能。
JS
import { shallowRef, shallowReactive, triggerRef } from 'vue'
// shallowRef:只跟踪 .value 整体替换,不跟踪内部属性
const bigData = shallowRef({ items: [...50000 items] })
bigData.value.items.push('new') // ❌ 不触发更新
bigData.value = { items: [newItems] } // ✅ 触发更新(整体替换)
// shallowReactive:只跟踪顶层属性,不深度响应
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
state.user.name = 'Bob' // ❌ 不触发
state.user = { name: 'Bob' } // ✅ 触发(顶层属性替换)
(2) 强制触发更新
JS
// 配合 Vue 全局 API 使用
const { triggerRef, shallowRef } = Vue
const bigData = shallowRef({ items: [] })
bigData.value.items.push('new') // 默认不响应
triggerRef(bigData) // 手动触发更新
典型场景:大表格(100,000 rows of)、大列表、频繁修改内部属性但只关心整体状态。
8. toRef / toRefs:解构保持响应式
(1) toRefs:reactive 对象的解构
JS
// 在 setup() 中从 Vue 解构
const { reactive, toRefs } = Vue
const state = reactive({
count: 0,
name: 'Alice'
})
// ❌ 普通解构:丢失响应
const { count, name } = state
// ✅ toRefs 解构:保持响应
const { count, name } = toRefs(state)
// 模板中使用
// <p>{{ count }}</p> <!-- 仍然响应 -->
(2) toRef:把普通值变成 ref
JS
// 在 setup() 中从 Vue 解构
const { toRef, ref } = Vue
// 场景:从 props 中拿一个字段
const props = defineProps({ count: Number })
// ❌ 直接解构丢失响应
const { count } = props // count 是普通 number
// ✅ toRef 包装
const count = toRef(props, 'count') // count 是 ref(props.count)
// 用法
count.value++ // 响应式修改 props
9. 响应式失效的 5 个陷阱
(1) 陷阱 1:直接替换 reactive 对象
JS
let state = reactive({ count: 0 })
// ❌ state 整个变量换了,但模板里用的是旧 state
state = reactive({ count: 1 })
// ✅ 修改属性
state.count = 1
(2) 陷阱 2:解构 reactive 丢失响应
JS
const state = reactive({ count: 0 })
// ❌ count 是普通变量
const { count } = state
// ✅ 用 toRefs
const { count } = toRefs(state)
(3) 陷阱 3:数组直接赋值
JS
// 在 setup() 中
const items = ref([1, 2, 3])
// ❌ 直接 push 不响应(要 triggerRef)
items.value.push(4)
// ✅ 整个替换
items.value = [...items.value, 4]
(4) 陷阱 4:直接修改 ref 包装的对象内部
JS
// 一般不会失效,但有边界情况
const user = ref({ name: 'Alice' })
user.value.name = 'Bob' // ✅ 响应(ref 包装对象会自动解包)
(5) 陷阱 5:ref vs reactive 类型混淆
JS
// ❌ 类型错误:reactive 不能包装原始值
const count = reactive(0) // 报错:value cannot be made reactive
// ✅ 用 ref
const count = ref(0)
10. 完整示例:响应式购物车
▶ 示例:ref 基础用法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const count = ref(0)
function increment() { count.value++ } // JS 中必须用 .value
function reset() { count.value = 0 }
return { count, increment, reset }
}
}).mount('#app')
</script>
▶ 示例:reactive 包装对象
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>User: {{ state.user.name }}</p>
<p>Age: {{ state.user.age }}</p>
<button @click="birthday">+1 Year</button>
</div>
<script>
const { createApp, reactive } = Vue
createApp({
setup() {
const state = reactive({ user: { name: 'Alice', age: 25 } })
function birthday() { state.user.age++ } // 直接修改,自动响应
return { state, birthday }
}
}).mount('#app')
</script>
▶ 示例:ref vs reactive 对比
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<h3>ref 风格</h3>
<p>ref.name: {{ userRef.name }} | ref.age: {{ userRef.age }}</p>
<button @click="updateRef">更新 ref</button>
<h3>reactive 风格</h3>
<p>reactive.name: {{ userReactive.name }} | reactive.age: {{ userReactive.age }}</p>
<button @click="updateReactive">更新 reactive</button>
</div>
<script>
const { createApp, ref, reactive } = Vue
createApp({
setup() {
// ref 风格
const userRef = ref({ name: 'Alice', age: 25 })
function updateRef() {
userRef.value.name = 'Bob' // ✅ 响应
userRef.value = { name: 'Charlie', age: 30 } // ✅ 响应
}
// reactive 风格
const userReactive = reactive({ name: 'Alice', age: 25 })
function updateReactive() {
userReactive.name = 'Bob' // ✅ 响应
// userReactive = reactive({...}) // ❌ 失效(整个变量替换)
}
return { userRef, userReactive, updateRef, updateReactive }
}
}).mount('#app')
</script>
▶ 示例:toRefs 解构保持响应
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>{{ count }} - {{ name }}</p>
<button @click="increment">+1</button>
</div>
<script>
const { createApp, reactive, toRefs } = Vue
createApp({
setup() {
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state) // 保持响应
function increment() { count.value++ } // count 是 ref,所以用 .value
return { count, name, increment }
}
}).mount('#app')
</script>
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
let state = reactive({...}); state = {...} |
整个 state 替换,模板不更新 | 修改属性 state.x = 1 |
const { count } = state 解构 |
count 变普通变量 | 用 toRefs(state) |
reactive(0) 包装原始值 |
报错 | 原始值用 ref() |
count.value++ 在 template |
报错 | template 直接 {{ count }} |
array[index] = x 直接赋值 |
可能不响应 | 用 splice 或整体替换 |
❓ 常见问题
Q ref 和 reactive 到底选哪个?
A 官方推荐全部用 ref(理由:ref 适用任何类型,整体替换、传 props/函数都更明确)。reactive 适用于"多个相关字段组成 state"的场景(像 Vuex)。新手推荐先学 ref,熟练后再用 reactive。
Q 为什么 ref 要
.value 而模板不需要?A
.value 是为了让 JS 引擎明确知道"这是一个 ref 包装的值,要触发响应式"。模板是 Vue 自己解析的,它知道哪些是 ref 所以自动解包。简而言之:JS 中需要 .value(避免歧义),模板中自动解包(方便)。Q ref 包装对象时,访问属性要 .value.x 吗?
A 不需要。
ref({name: 'Alice'}) 后,模板直接 {{ user.name }},JS 中 user.value.name。Vue 自动处理了 ref 包装对象的内部属性访问。Q reactive 解构会丢失响应式,怎么解决?
A 用
toRefs() 包装。const { count, name } = toRefs(state) 之后,count 和 name 变成 ref,模板中 {{ count }},JS 中 count.value,都保持响应。Q shallowRef 和 ref 的区别?
A
ref 是深度响应(内部属性变化也触发),shallowRef 只跟踪 .value 整体替换(不跟踪内部)。当数据很大(10万+条)且只关心整体替换时,用 shallowRef 性能更好。Q 响应式失效的常见原因?
A 5 个最常见:(1) 直接替换 reactive 对象;(2) 解构 reactive 不用 toRefs;(3) ref 包装原始值不响应(应改用 reactive 或保持 ref);(4) 在 setup 外创建 ref(失去响应);(5) 数组直接赋值(应用 splice)。
Q reactive 可以包装 Map/Set 吗?
A 可以。Vue 3 用 Proxy 实现的 reactive 支持 Map/Set/WeakMap/WeakSet。Vue 2 不支持。
📖 小节
- Vue 3 响应式系统核心:Proxy 拦截 + 依赖收集(track)+ 触发更新(trigger)
ref()包装任何值,JS 访问需.value,模板自动解包reactive()包装对象(Object/Array),深度响应,无 .value- ref vs reactive 选择:ref 万能且推荐,reactive 适合"多字段 state"
- 5 大陷阱:替换 reactive 对象 / 解构丢失响应 / 原始值用 reactive / 数组直接赋值 / 类型混淆
- shallowRef / shallowReactive 用于大对象性能优化
- toRefs 解构保持响应,toRef 把普通值转 ref
📝 作业
-
基础题(难度⭐) 用 ref 写一个简单的购物车数量计数器:
- 初始值 1
- "+" 按钮加 1
- "-" 按钮减 1
- 数量 ≤ 0 时禁用 "-" 按钮(:disabled 动态)
- 数量 ≥ 99 时禁用 "+" 按钮
-
进阶题(难度⭐⭐) 用 reactive 写一个用户信息编辑表单:
- state 含
name、email、age三个字段 - 3 个输入框双向绑定(@input 事件,下一课学 v-model)
- "重置" 按钮恢复初始值
- "提交" 按钮在控制台打印完整 state
- state 含
-
挑战题(难度⭐⭐⭐) 实现一个完整的 Todo 应用(数据层):
- 用 reactive 包装 todos 数组
- 每个 todo 含
{ id, text, done } - 提供方法:addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
- 提供计算属性:activeCount(未完成数)、doneCount(完成数)
- 在模板中显示完整 todo 列表 + 计数
数据初始:
{ id: 1, text: 'Learn Vue 3', done: false }