响应式系统深入理解
Vue 的响应式系统,决定了组件状态怎么追踪、什么时候更新、哪些写法更稳、哪些写法容易出错。
理解这部分之后,很多看起来像“框架魔法”的行为,其实都会变得很直白。
ref 和 reactive
ref
适合包装单个值。
import { ref } from 'vue'
const count = ref(0)
count.value++
常见场景:
- 数字、字符串、布尔值
- DOM 引用
- 单个对象引用
- 需要在函数之间传来传去的响应式值
reactive
适合包装对象结构。
import { reactive } from 'vue'
const form = reactive({
name: '',
email: '',
})
常见场景:
- 表单对象
- 复杂筛选条件
- 多字段联动状态
两者怎么选
更实用的判断不是“谁更高级”,而是“状态形状是什么”。
- 单个值:
ref - 一组强相关字段:
reactive - 需要显式拆分和传递:
ref往往更灵活
computed
computed 用来声明派生状态。
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
它和普通函数的区别在于:
- 会缓存结果
- 只有依赖变化时才重新计算
- 语义更明确,一看就知道这是“派生值”
复杂页面里,computed 往往比“在模板里塞一大串表达式”更稳,也比“手动维护一个额外 state”更省心。
watch 和 watchEffect
watch
适合“明确盯某个源,再决定怎么响应”。
watch(
() => route.params.id,
async (id) => {
await fetchDetail(id)
},
{ immediate: true }
)
适合:
- 接口请求
- 参数联动
- 表单字段联动
- 本地缓存同步
watchEffect
适合“副作用依赖谁,由运行时自动收集”。
watchEffect(() => {
console.log(filters.keyword)
})
它写起来更快,但边界也更松。如果副作用比较重 、依赖比较多,后期排查起来往往不如 watch 直观。
onWatcherCleanup()
这是 3.5 很值得补的一点。
在 watch 或 watchEffect 里,经常会遇到一个问题:上一次副作用还没结束,下一次就来了。典型例子是接口切换、输入搜索、定时器和订阅清理。
watch(searchKeyword, async (keyword) => {
const controller = new AbortController()
onWatcherCleanup(() => {
controller.abort()
})
const res = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal,
})
data.value = await res.json()
})
这比在外面手动维护一堆中断逻辑更干净。
toRef、toRefs、toValue
toRef
适合把对象上的某个字段单独抽出来,同时保留响应式联系。
toRefs
适合把整个响应式对象展开成多个 ref。
toValue
统一处理“值、ref、getter”这几类输入时很方便。
这类 API 的价值,在写 composables 时最明显。很多“参数到底传值还是传 ref”的麻烦,都可以靠它们收一层。
常见误区
1. reactive 解构后直接用
const state = reactive({ count: 0 })
const { count } = state
这种写法会丢掉响应式联系。需要保留时,要用 toRef 或 toRefs。
2. 把所有副作用都写成 watchEffect
watchEffect 很顺手,但不是越多越好。只要副作用重、依赖复杂、清理敏感,watch 往往更稳。
3. 把 computed 当缓存仓库
computed 适合纯派生,不适合做复杂副作用。它应该像“声明式公式”,不是“偷偷做很多事的地方”。
更实用的理解方式
Vue 响应式系统可以简单理解成两层:
- 第一层:谁是状态源
- 第二层:谁依赖了这个状态源
状态一变化,Vue 只更新真正依赖它的那部分视图或副作用。很多性能和可维护性问题,本质上都是“状态边界没画清楚”。