Vue.js: 响应式数据:ref/reactive

最后更新:2026-08-26

Vue 3 的响应式系统是它的灵魂——当你修改一个变量,页面上所有用到它的地方自动更新。这就是为什么 Vue 写起来比 jQuery 简单 10 倍。响应式系统由两个核心 API 支撑:ref()reactive()

ref 用于包装任何类型的数据(数字、字符串、对象),reactive 只能包装对象。两者用法不同,本课帮你彻底搞懂。

1. 你将学到


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 响应式后:


3. 响应式原理:Proxy + 依赖收集

(1) 3 步响应流程

100%
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) 关键点

(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 解构会丢失响应式,怎么解决?
AtoRefs() 包装。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 不支持。

📖 小节


📝 作业

  1. 基础题(难度⭐) 用 ref 写一个简单的购物车数量计数器:

    • 初始值 1
    • "+" 按钮加 1
    • "-" 按钮减 1
    • 数量 ≤ 0 时禁用 "-" 按钮(:disabled 动态)
    • 数量 ≥ 99 时禁用 "+" 按钮
  2. 进阶题(难度⭐⭐) 用 reactive 写一个用户信息编辑表单:

    • state 含 nameemailage 三个字段
    • 3 个输入框双向绑定(@input 事件,下一课学 v-model)
    • "重置" 按钮恢复初始值
    • "提交" 按钮在控制台打印完整 state
  3. 挑战题(难度⭐⭐⭐) 实现一个完整的 Todo 应用(数据层):

    1. 用 reactive 包装 todos 数组
    2. 每个 todo 含 { id, text, done }
    3. 提供方法:addTodo(text) / toggleTodo(id) / removeTodo(id) / clearDone()
    4. 提供计算属性:activeCount(未完成数)、doneCount(完成数)
    5. 在模板中显示完整 todo 列表 + 计数

    数据初始:{ id: 1, text: 'Learn Vue 3', done: false }

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏