跳到主要内容

响应式系统深入理解

Vue 的响应式系统,决定了组件状态怎么追踪、什么时候更新、哪些写法更稳、哪些写法容易出错。

理解这部分之后,很多看起来像“框架魔法”的行为,其实都会变得很直白。

refreactive

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”更省心。

watchwatchEffect

watch

适合“明确盯某个源,再决定怎么响应”。

watch(
() => route.params.id,
async (id) => {
await fetchDetail(id)
},
{ immediate: true }
)

适合:

  • 接口请求
  • 参数联动
  • 表单字段联动
  • 本地缓存同步

watchEffect

适合“副作用依赖谁,由运行时自动收集”。

watchEffect(() => {
console.log(filters.keyword)
})

它写起来更快,但边界也更松。如果副作用比较重、依赖比较多,后期排查起来往往不如 watch 直观。

onWatcherCleanup()

这是 3.5 很值得补的一点。

watchwatchEffect 里,经常会遇到一个问题:上一次副作用还没结束,下一次就来了。典型例子是接口切换、输入搜索、定时器和订阅清理。

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()
})

这比在外面手动维护一堆中断逻辑更干净。

toReftoRefstoValue

toRef

适合把对象上的某个字段单独抽出来,同时保留响应式联系。

toRefs

适合把整个响应式对象展开成多个 ref

toValue

统一处理“值、ref、getter”这几类输入时很方便。

这类 API 的价值,在写 composables 时最明显。很多“参数到底传值还是传 ref”的麻烦,都可以靠它们收一层。

常见误区

1. reactive 解构后直接用

const state = reactive({ count: 0 })
const { count } = state

这种写法会丢掉响应式联系。需要保留时,要用 toReftoRefs

2. 把所有副作用都写成 watchEffect

watchEffect 很顺手,但不是越多越好。只要副作用重、依赖复杂、清理敏感,watch 往往更稳。

3. 把 computed 当缓存仓库

computed 适合纯派生,不适合做复杂副作用。它应该像“声明式公式”,不是“偷偷做很多事的地方”。

更实用的理解方式

Vue 响应式系统可以简单理解成两层:

  • 第一层:谁是状态源
  • 第二层:谁依赖了这个状态源

状态一变化,Vue 只更新真正依赖它的那部分视图或副作用。很多性能和可维护性问题,本质上都是“状态边界没画清楚”。