大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
响应式进阶
这一课你将能做到
- 用
ref和reactive创建响应式数据,并知道何时用哪个 - 用
computed派生计算属性 - 用
watch和watchEffect监听数据变化 - 用
toRef/toRefs解构响应式对象不丢失响应性
为什么要学
响应式是 Vue 的「心脏」。你写的每一行 count.value++,背后都是 Vue 的依赖追踪和视图更新。不理解响应式,写出的代码要么不更新,要么更新过度。
人话:响应式就像「自动记账本」。你告诉 Vue「这个数据很重要」,之后每次数据变动,Vue 自动更新所有用到它的地方,不用你手动通知。
先建立图像
<script setup>
import { ref, computed, watch } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变成 ${newVal}`)
})
</script>
<template>
<p>原始:{{ count }}</p>
<p>双倍:{{ double }}</p>
<button @click="count++">+1</button>
</template>
跟着做
步骤 1:ref 包装基本类型
<script setup>
import { ref } from 'vue'
const num = ref(0)
const str = ref('hello')
const bool = ref(true)
function change() {
num.value = 100 // 正确:通过 .value 修改
str.value = 'world'
}
</script>
ref 适合包装数字、字符串、布尔值,也可以包装对象。
步骤 2:reactive 包装对象
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: '张三',
age: 25,
address: { city: '北京' }
})
function update() {
user.age = 26 // 直接改属性,不用 .value
user.address.city = '上海' // 深层也响应式
}
</script>
reactive 只能包装对象/数组,不能包装基本类型。
步骤 3:computed 计算属性
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const count = ref(2)
// 只读计算属性
const total = computed(() => price.value * count.value)
// 可写计算属性
const totalWritable = computed({
get: () => price.value * count.value,
set: (val) => { price.value = val / count.value }
})
</script>
computed 有缓存,依赖不变时不重复计算。
步骤 4:watch 监听变化
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 监听单个 ref
watch(keyword, (newVal, oldVal) => {
console.log('搜索:', newVal)
})
// 监听多个源
watch([keyword, () => props.id], ([newKw, newId]) => {
fetchData(newKw, newId)
})
// 深度监听
const obj = ref({ a: 1 })
watch(obj, (val) => console.log(val), { deep: true })
</script>
步骤 5:watchEffect 自动追踪
<script setup>
import { ref, watchEffect } from 'vue'
const url = ref('https://api.example.com')
const data = ref(null)
// 自动追踪 url 变化,立即执行一次
watchEffect(async () => {
const res = await fetch(url.value)
data.value = await res.json()
})
</script>
watchEffect 自动收集依赖,适合「只要依赖变就执行」的场景。
步骤 6:toRef / toRefs 解构不丢响应性
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({ name: '张三', age: 25 })
// 错误:解构后丢失响应性
// const { name, age } = state
// 正确:toRefs 保持响应性
const { name, age } = toRefs(state)
</script>
讲清楚要点
- ref vs reactive:ref 万能(基本类型 + 对象),reactive 只能对象但不用
.value。优先 ref。 - computed 有缓存:依赖不变时直接返回上次结果,性能比方法好。
- watch 是惰性监听:只有依赖变化时才执行;
watchEffect立即执行一次。 - deep 监听有性能代价:对象很大时慎用,考虑只监听特定属性。
- toRefs 解决解构丢失响应性:从 reactive 对象解构时用。
你可能会懵的地方
-
ref 对象在模板里为什么不用
.value? Vue 编译模板时自动对顶层 ref 解包,但嵌套在对象里不会。 -
computed 和方法有什么区别? computed 有缓存,依赖不变时不重复执行;方法每次渲染都执行。
-
watch 和 watchEffect 怎么选? 需要精确控制监听哪些数据用 watch;需要立即执行且自动追踪依赖用 watchEffect。
-
为什么 reactive 对象解构后不是响应式? 解构得到的是普通值,不是 Proxy 引用。用
toRefs保持引用。 -
ref 的 .value 在模板里什么时候要写? 模板里不写,JS 里写。但
<template>里嵌套的 ref 如obj.count.value要写。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 用 reactive 包装基本类型 | 不是响应式 | 用 ref |
在模板里写 count.value | 多余且报错 | 直接 {{ count }} |
| 解构 reactive 对象 | 丢失响应性 | 用 toRefs |
| 用方法代替 computed | 每次渲染都执行,性能差 | 用 computed |
| watch 监听整个大对象加 deep | 性能差 | 监听具体属性 |
卡点急救
- 数据变了视图没变:检查是否用 ref/reactive 声明,普通变量不是响应式。
- computed 不更新:检查依赖是否也是响应式数据,或是否写成了
computed(() => { ... })忘了 return。 - watch 没触发:检查监听源是否正确,ref 要写成
() => xxx.value或直接用 ref 本身。
面试常问
-
Vue 3 响应式原理? 基于
Proxy代理对象,拦截 get/set,配合effect收集依赖,数据变化时通知更新。 -
ref 和 reactive 的底层实现? ref 内部用
reactive包装{ value: xxx };reactive 直接用Proxy代理原对象。 -
computed 和 watch 的区别? computed 派生新值,有缓存;watch 执行副作用,不返回值。
自测三问
ref和reactive分别适合什么数据类型?computed相比方法的优势是什么?- 什么时候必须用
watchEffect而不是watch?
小结一句话
响应式是 Vue 的根基,ref/reactive 定义数据,computed 派生数据,watch/watchEffect 监听变化,掌握它们就掌握了 Vue 的数据流。
延伸
- Vue 官方文档 - 响应式基础
- 下一课:生命周期——组件的出生、更新与销毁。