跳转到正文
YGJoon 技术博客

Vue 3 组合式 API 入门:ref 还是 reactive,先把这道选择题做对

发布于:
·1.4k 字 · 约 3 分钟·作者:YGJoon
ref 和 reactive 怎么选?从响应式原理讲起,用计数器、表单、列表三个场景讲透 computed、watch 和生命周期。
Vue 3 组合式 API 入门:ref 还是 reactive,先把这道选择题做对文章封面
目录

Vue 3 的组合式 API(Composition API)配合 <script setup> 已经是现在写 Vue 的默认姿势。但初学者最常卡住的不是语法,而是第一道选择题:这个数据到底用 ref 还是 reactive?

先给结论,再讲为什么:默认用 ref,只有明确需要”一个整体对象”时才用 reactive。比如表单对象、分页查询参数这种天然就是一坨的东西。

ref 与 reactive 的本质区别

ref 可以包装任何类型的值,模板里自动解包,JS 里要 .value:

<script setup>
import { ref } from 'vue'

const count = ref(0)
const name = ref('joon')

function increment() {
  count.value++ // JS 里必须 .value
}
</script>

<template>
  <!-- 模板里直接用,不用 .value -->
  <p>{{ count }} - {{ name }}</p>
  <button @click="increment">+1</button>
</template>

reactive 只能包装对象(包括数组),返回的是原对象的 Proxy:

<script setup>
import { reactive } from 'vue'

const form = reactive({
  username: '',
  password: '',
  remember: true,
})
</script>

<template>
  <input v-model="form.username" />
  <input v-model="form.password" type="password" />
</template>

注意 reactive 有两个硬伤,新手必踩:

  1. 解构会丢失响应性:const { username } = form 拿到的就是普通字符串了。非要解构,用 toRefs(form)。
  2. 不能整体替换:form = { username: 'a' } 直接报错(const)或断开响应。只能逐个字段赋值,或者 Object.assign(form, newData)。
// ❌ 错误:解构丢失响应性
const { username } = form
username = 'new' // 界面不会更新

// ✅ 正确:保持 reactive 整体使用
form.username = 'new'

// ✅ 批量更新用 Object.assign
Object.assign(form, { username: 'a', password: 'b' })

所以回到开头那道选择题:单个值、可能被替换的值 → ref;一组固定字段、整体传递的对象 → reactive。拿不准就用 ref,ref 包对象一样能用(ref({}) 内部也是 reactive),只是多写 .value。

computed:派生状态别用 watch 硬算

只要”一个值是由其他值算出来的”,就用 computed,别用 watch 手动同步:

<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { name: '键盘', price: 299, checked: true },
  { name: '鼠标', price: 149, checked: false },
])

// ✅ computed:声明式,自动缓存
const total = computed(() =>
  items.value.filter(i => i.checked).reduce((s, i) => s + i.price, 0)
)

// ❌ 别这样:watch 手动同步又啰嗦又容易漏
</script>

<template>
  <p>合计:¥{{ total }}</p>
</template>

computed 带缓存,只有依赖变了才重算。需要”可写”的派生值时给它 getter/setter,比如全选框:

const allChecked = computed({
  get: () => items.value.every(i => i.checked),
  set: (v) => items.value.forEach(i => (i.checked = v)),
})

watch vs watchEffect:别混着用

import { ref, watch, watchEffect } from 'vue'

const keyword = ref('')
const page = ref(1)
const list = ref([])

// 明确监听 keyword,防抖搜接口
watch(keyword, (newVal) => {
  searchApi(newVal)
})

// 自动追踪:keyword 或 page 任一变化都重新拉列表
watchEffect(async () => {
  list.value = await fetchList({ q: keyword.value, page: page.value })
})

两个细节:watch 默认是懒执行的(数据源变了才跑),想要立即跑一次加 { immediate: true };监听 reactive 对象默认是深层监听,监听 ref 包的对象需要 { deep: true }。

生命周期:写在 setup 里,没有 this

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'

const width = ref(window.innerWidth)

function onResize() {
  width.value = window.innerWidth
}

onMounted(() => {
  window.addEventListener('resize', onResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', onResize)
})
</script>

常用的就这几个:onMounted(发请求、绑事件)、onUnmounted(清定时器、解绑)、onUpdated 很少用。注意组合式 API 里没有 this,created 对应的逻辑直接写在 setup 顶层就行。

和 Options API 的对照

Options APIComposition API
data()ref / reactive
computed: {}computed()
watch: {}watch() / watchEffect()
methods: {}普通函数
mounted()onMounted()
this.xxx直接用变量名

组合式 API 真正的价值不是语法糖,而是逻辑复用:把”搜索框的防抖逻辑""分页逻辑”抽成 useSearch()、usePagination() 这种 composable 函数,跨组件直接 import。Options API 时代靠 mixins 做这事,命名冲突和来源不明是出了名的坑。

实战案例

来自个人全栈项目的一个后台管理页面:用户列表,带搜索框、分页、批量删除。

一开始我把搜索条件写成了一个 reactive 对象 { keyword: '', status: '', page: 1 },翻页时想重置条件,直接 query = { keyword: '', status: '', page: 1 } —— 界面纹丝不动。原因就是上面说的 reactive 不能整体替换。改成 Object.assign(query, { keyword: '', status: '', page: 1 }) 就好了。

第二个坑是搜索防抖。我一开始用 watchEffect 监听 keyword,每敲一个字母就请求一次接口。后改成 watch(keyword, debounce(fn, 300)),明确只监听 keyword 且防抖,请求量直接降了一个数量级。

现在的习惯:表单类整体对象用 reactive,其他一律 ref;派生值无脑 computed;副作用明确数据源用 watch,图省事才用 watchEffect。这套规则跑了小半年,没再踩过响应性丢失的坑。


分享这篇文章:


相关文章

下一篇
Nuxt 3 服务端渲染实践:路由、数据获取与水合避坑