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