今天的目标非常明确:彻底搞懂 Vue 3 响应式系统的四大基石。只要掌握了这四个 API,你就掌握了 Vue 3 数据流的核心。
我们将通过概念解析 + 代码对比 + 避坑指南的方式,确保你不仅“会用”,还能“懂原理”。
1. ref:万物皆可 Ref
核心定义:用于定义响应式变量。它可以包裹基本数据类型(String, Number, Boolean),也可以包裹对象或数组。
💡 关键规则
- JS 中访问:必须加
.value(例如count.value)。 - 模板中访问:自动解包,不需要
.value(例如{{ count }})。 - 适用场景:首选方案。无论是数字还是对象,用
ref最统一,重构时最安全。
📝 代码示例
<script setup>
import { ref } from 'vue'// 1. 基本类型
const count = ref(0)
const message = ref('Hello Vue 3')// 2. 对象/数组 (虽然 reactive 也能做,但 ref 更通用)
const user = ref({ name: 'Alice', age: 25 })
const hobbies = ref(['Coding', 'Gaming'])// ⚠️ 注意:在 JS 逻辑中修改,必须加 .value
const increment = () => {count.value++ user.value.age = 26 // 修改对象属性hobbies.value.push('Reading')
}
</script><template><div><!-- 模板中自动解包,直接写 count --><p>计数: {{ count }}</p> <p>消息: {{ message }}</p><p>用户年龄: {{ user.age }}</p><button @click="increment">点我增加</button></div>
</template>
2. reactive:专为对象设计
核心定义:用于定义对象或数组类型的响应式数据。它基于 ES6 Proxy 实现。
💡 关键规则
- JS 中访问:不需要
.value,直接操作属性(例如state.count)。 - 限制:
- 只能接收对象/数组,不能包裹基本类型。
- 解构会丢失响应性(除非配合
toRefs使用,见下文避坑指南)。 - 不能直接替换整个对象(
state = newObj会失效),必须修改属性。
📝 代码示例
<script setup>
import { reactive } from 'vue'const state = reactive({count: 0,user: { name: 'Bob' }
})const update = () => {state.count++ // ✅ 正确:直接修改属性state.user.name = 'Tom' // ✅ 正确// state = { count: 1 } // ❌ 错误:这样会切断响应式连接!
}
</script>
⚖️ ref vs reactive 怎么选?
| 特性 | ref |
reactive |
|---|---|---|
| 数据类型 | 任意 (基本类型 + 对象) | 仅限 对象/数组 |
| 访问方式 | JS 中需 .value |
JS 中直接访问 |
| 替换值 | count.value = 1 (✅ 支持) |
state = {} (❌ 不支持) |
| 解构 | 天然支持 (const { c } = refObj) |
需 toRefs 辅助 |
| 2026 建议 | 🏆 推荐默认使用 ref |
仅在确定不需要替换整个对象时使用 |
💡 专家建议:为了代码风格统一和避免“解构丢失响应性”的坑,现代 Vue 开发(尤其是配合 TypeScript 时)倾向于全部使用
ref。
3. computed:带缓存的计算属性
核心定义:基于其他响应式数据计算得出的新数据。只有依赖变化时才会重新计算,否则直接返回缓存值。
💡 关键规则
- 只读性:默认是只读的(Getter)。如果需要修改(Setter),需传入对象。
- 自动追踪依赖:你在函数里用了哪些
ref或reactive,它自动就知道要监听谁。 - 性能:比在模板里写复杂表达式或在
methods里计算更高效。
📝 代码示例
<script setup>
import { ref, computed } from 'vue'const price = ref(100)
const quantity = ref(2)
const taxRate = ref(0.1)// 计算总价 (自动监听 price, quantity, taxRate)
const total = computed(() => {console.log('计算执行了...') // 只有依赖变了才会打印return price.value * quantity.value * (1 + taxRate.value)
})// 高级用法:可写的计算属性 (Getter + Setter)
const doublePrice = computed({get: () => price.value * 2,set: (val) => { price.value = val / 2 }
})
</script><template><p>单价: {{ price }}</p><p>数量: {{ quantity }}</p><p>总价 (含税): {{ total }}</p><button @click="doublePrice = 400">设置双倍价格为 400 (单价会变 200)</button>
</template>
4. watch & watchEffect:监听副作用
核心定义:当数据变化时,执行某些副作用(如:发送 API 请求、操作 DOM、打印日志)。
A. watch (精准监听)
- 特点:懒执行(数据变了才跑),可以指定监听特定源,能获取新旧值。
- 场景:路由变化、表单输入防抖、特定数据变更触发逻辑。
import { ref, watch } from 'vue'const searchQuery = ref('')// 监听单个 ref
watch(searchQuery, (newVal, oldVal) => {console.log(`搜索词从 "${oldVal}" 变成了 "${newVal}"`)// 这里可以发起 API 请求
})// 监听多个源
watch([searchQuery, someOtherRef], ([newQ, newOther], [oldQ, oldOther]) => {// ...
})
B. watchEffect (立即执行 + 自动收集依赖)
- 特点:立即执行一次,自动追踪函数内部用到的所有响应式数据。不用显式指定监听谁。
- 场景:初始化加载数据、简单的日志记录。
import { ref, watchEffect } from 'vue'const userId = ref(1)// 只要 userId 变了,或者函数内用到的其他 ref 变了,都会重跑
watchEffect(() => {console.log(`当前用户 ID: ${userId.value}`)// fetchUser(userId.value)
})
// ⚠️ 注意:上面代码组件挂载时会立即执行一次,无需手动调用
⚖️ watch vs watchEffect 怎么选?
| 特性 | watch |
watchEffect |
|---|---|---|
| 执行时机 | 懒执行 (数据变后才跑) | 立即执行 (挂载时先跑一次) |
| 依赖来源 | 显式指定 (第一个参数) | 隐式自动收集 (函数里用了谁就监听谁) |
| 获取旧值 | ✅ 支持 (new, old) |
❌ 不支持 |
| 推荐场景 | 需要旧值、防抖、特定字段变更 | 简单的副作用、初始化请求 |
🚫 Day 1 避坑指南 (必读!)
-
reactive解构陷阱:const state = reactive({ count: 0 }) let { count } = state // ❌ 错误!count 变成了一个普通数字,失去响应性// ✅ 正确做法 1 (推荐): 直接用 state.count // ✅ 正确做法 2: 使用 toRefs import { toRefs } from 'vue' const { count } = toRefs(state) // 现在 count 是一个 ref,有响应性这也是为什么建议大家多用
ref的原因,ref解构天然安全(在<script setup>中)。 -
忘记
.value:
在<script>里操作ref时,90% 的 bug 都是因为忘了写.value。count++ // ❌ 无效,只是修改了一个普通数字 count.value++ // ✅ 正确 -
在
computed里做异步操作:
computed必须同步返回一个值。如果你需要在数据变化时发请求,请用watch或watchEffect。
🎯 今日实战练习
请尝试在脑海中(或编辑器里)完成以下小任务:
- 创建一个
ref名为score,初始值为 0。 - 创建一个
computed名为level,逻辑是:score > 10返回 "High",否则 "Low"。 - 创建一个
watch监听score,当分数变化时,在控制台打印 "分数更新了"。 - 创建一个按钮,点击让
score加 5。
思考题:如果我把 score 改成 reactive({ val: 0 }),上面的代码需要怎么改?
