你写 Vue3 时经常会遇到这些"似懂非懂"的问题:
- 为什么
ref需要.value? - 为什么
reactive解构后就"不响应"了? watch、watchEffect、computed到底怎么选?
这篇不从源码堆概念,而是用"你在项目里一定会踩的坑"为主线,把 Vue3 响应式讲清楚。
1. 响应式的目标是什么
目标只有一个:
- 当某个状态发生变化时,能定位到"谁用到了它",并触发对应的更新。
对应两个关键动作:
- 依赖收集:谁在读取这个状态?
- 派发更新:这个状态变了,要通知谁重新执行?
Vue3 用 Proxy/Reflect + effect(副作用函数)完成这件事。
2. reactive:对象的响应式
reactive(obj) 返回一个 Proxy:
- 读取属性(
get)时:收集依赖 - 修改属性(
set)时:触发更新
适合:
- 状态本身是一个对象/数组
- 需要深层属性自然响应
2.1 常见坑:解构丢响应
js
const state = reactive({ count: 0 })
const { count } = state
count++ // 这里不会触发视图更新
原因:解构得到的是一个普通值,不再经过 Proxy 的 get。
解决:
- 用
toRefs(state)/toRef(state, 'count')
js
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0 })
const { count } = toRefs(state)
count.value++
3. ref:单值/原始类型的响应式
ref(0) 返回形如 { value: 0 } 的对象,它本身也会被响应式处理。
为什么要有 .value:
- 为了让原始类型也能用同一套"依赖收集/触发更新"的机制
适合:
- 数字/字符串/布尔值等原始类型
- 你希望"一个变量就是一个状态"
3.1 ref 包对象会怎样
js
const user = ref({ name: 'a' })
user.value.name = 'b' // 也能响应
本质上:ref 内部会把对象也做成响应式(类似 reactive)。
4. computed:带缓存的派生状态
特点:
- 懒执行:没人读就不算
- 有缓存:依赖不变不重新算
适合:
- 由多个状态推导出的展示字段
- 避免把"计算逻辑"散落在模板里
5. watch vs watchEffect
5.1 watch
- 你明确知道要监听谁(source)
- 你需要 old/new
- 你需要精细控制(immediate / deep / flush)
js
watch(
() => state.keyword,
(newVal, oldVal) => {
fetchList(newVal)
},
{ immediate: true }
)
5.2 watchEffect
- 你不想声明 source
- 只要 effect 内读取到的响应式数据变化,就重新执行
js
watchEffect(() => {
fetchList(state.keyword)
})
注意:watchEffect 很方便,但也更容易"无意间读到多余依赖"导致频繁触发。
6. 实战建议(项目里最实用的选择策略)
- 状态是对象/表单 :优先
reactive,配合toRefs暴露字段 - 状态是单值 :优先
ref - 派生展示字段 :用
computed - 请求联动 :优先
watch(能控粒度、能拿 old/new、好排查) - 自动收集依赖的副作用 :再用
watchEffect
7. 常见坑与排查
- 响应式失效 :优先检查"是否解构了
reactive" - watch 不触发 :检查 source 是否是 getter(
watch(() => obj.x, ...)) - 频繁触发 :检查
watchEffect内是否读取了多余状态
8. 总结
- Vue3 响应式核心是:依赖收集 + 派发更新
reactive适合对象,但解构会丢响应,需要toRefsref让原始类型也能响应式computed做派生字段,带缓存watch更可控,watchEffect更省事但更容易过度依赖