Vue3 中 ref 和 reactive 怎么选

Vue 3 刚出那会儿我还带着 Vue 2 的 data 思维,把表格组件写错了。这篇记 ref 和 reactive 我现在怎么选、reactive 当函数入参和解构时丢响应性的两次翻车,还有团队约定为什么能省一半心智。
ref 和 reactive 我第一次用反
Vue 3 刚出那会儿,我脑子里的认知模型还是 Vue 2 的 data。我把一个表格组件写成这样:
import { ref } from 'vue'
export default {
setup() {
const tableData = ref([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(20)
const filter = ref({ keyword: '', status: 'all' })
async function load() {
loading.value = true
const r = await fetchData({ page: page.value, pageSize: pageSize.value, ...filter.value })
tableData.value = r.items
loading.value = false
}
return { tableData, loading, page, pageSize, filter, load }
}
}同事看了一眼摇头:「你这个 filter 用 reactive 更好啊。」他大概意思是「filter 是一组字段」。我听半懂不懂,反问「reactive 跟 ref 区别到底是啥?」
答案我后来自己看官方文档才整明白。没有绝对高下,看的是语义。
一个最小化对比
import { ref, reactive } from 'vue'
const count = ref(0)
const user = reactive({ name: '', role: 'guest' })
count.value++
user.name = 'admin'ref(0) 包的是数字,reactive({}) 包的是对象。两者底层都是 Proxy(ref 在简单值外面包一层 reactive),差别只剩「语法」和「团队认知」。
模板里:
<button @click="count++">{{ count }}</button>
<input v-model="user.name" />模板自动解包,不需要 .value。
我现在用的判断标准
我从那几个翻车后,整理出三个问题来判断用哪个:
问题一:这个状态是单独一个值,还是一组字段?
- 单独一个值(计数、开关、字符串、指针)→
ref - 一组字段(表单、筛选条件、对象)→
reactive
const count = ref(0) 比 const state = reactive({ count: 0 }) 更直接。
问题二:这个值会经常被整体替换吗?
- 整替(接口返回新对象)→
ref - 字段改(修改嵌套字段)→
reactive
const user = ref(null)
async function reload() {
user.value = await fetchUser() // 整体换
}const form = reactive({ ... })
function submit() {
form.username = '' // 改字段,不换对象
await save(form)
}reactive 用整个对象作为身份。如果你整体替换(form.value = {...} 这种语法在 reactive 下不行,必须 Object.assign(form, newForm)),它会破坏响应性追踪。
问题三:要在 composable 里返回吗?
composable返回 → 多用ref,因为消费者拿到的是稳定引用- 内部用 → 看场景
function useUser() {
const user = ref(null)
const loading = ref(false)
async function load() {
loading.value = true
user.value = await fetchUser()
loading.value = false
}
return { user, loading, load }
}这是社区里的主流写法。reactive 形态很难 SSR 安全(响应性丢失会留坑),ref 在 vitest/ssr 这些上下文里更稳。
我真的翻车过:reactive 函数入参
function setForm(initial) {
const form = reactive(initial)
watch(form, (v) => saveDraft(v))
return form
}我以为 watch(form, ...) 监听的是整个对象,结果发现深度监听嵌套字段根本没触发。原因:watch 默认是浅比较。如果 form.user.name 变了,watch(form) 的回调不会被调用,必须 watch(form, ..., { deep: true })。
watch(form, (v) => saveDraft(v), { deep: true })我后来索性 ref 一起返回:
function setForm(initial) {
const form = ref({ ...initial })
watch(form, (v) => saveDraft(v.value), { deep: true })
return form
}ref 里再包 deep: true 比 reactive 行为更显式。
第二个翻车:reactive 解构丢失响应性
最常踩。我以前以为 const state = reactive({ count: 0 }) 然后 return { ...state } 就把响应式带出去了,结果模板里 state.count 是响应的,但解构后的 count 反而不是。这是新人最常见的 Vue 3 谜之 bug。修法是 toRefs:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: '' })
return { ...toRefs(state) }模板里 count 自动解包。ref 形态就没这个坑,因为 ref 是稳定引用,不会被解构破坏。
团队约定能省一半心智
我现在的项目里就一条:所有状态都用 ref。
const count = ref(0)
const user = ref(null)
const form = ref({ username: '', password: '' })
const list = ref([])reactive 只在「明显是一个对象图、要一起 reactive 化、不会整体替换」的场景用,比如 Vuex 内部的 state、Pinia store。
理由:
- 团队新人不用纠结「这玩意到底用哪个」
ref的语法在所有场景都是一致的,认知负担小- 唯一例外是 Pinia store,那里用
reactive反而自然,因为 store 本质是个全局状态图
一段我没有自动解包的小代码
import { ref } from 'vue'
const user = ref({ name: 'tiana', role: 'admin' })
console.log(user.value.name)模板里 user.name 自动解包,但 js 代码里必须 user.value.name。
这个区别当年让我调了一会儿才发现。模板自动解包是为了人类可读,.value 是给机器的明确信号。我知道这是为了一致性(ref 形态不区分基本类型/对象),但新人期确实有学习成本。
反 Vue2 思维的一些坑
我带着 Vue 2 的脑子写过:
const list = reactive([])
list.push({ id: 1 }) // 看起来跟 Vue 2 一样确实可以。但后面 list = [] 这种重赋值就不响应了:
list = [] // 不响应,reactive 不允许重新赋值整个变量ref 形态就完全绕开这个:
const list = ref([])
list.value = []我现在的文章草稿框里清空用的就是 list.value = [],因为响应性不会断。
我现在写 Vue 3 时的体感
ref 是主力,reactive 是例外。我现在用 reactive 的理由只剩两个:一个是 Pinia store,那里用 reactive 反而不别扭;另一个是会跟 toRefs 配合批量展开成 ref 的对象。除了这两个场景,其他一律 ref。新人接手时不用思考,看到 ref 就知道「这是个状态」。这比纠结选哪个更省精力。