Vue 3 组合式 API 中响应式对象与 ref 的使用边界
先说结论:能用 ref 就别急着 reactive
我一开始写组合式 API 的时候,特别喜欢 reactive,原因很简单:不用到处写 .value,模板里直接 state.count,脚本里也直接 state.count++,看起来干净。后来被坑了几次才发现,这个“干净”是有代价的。
reactive 只适合对象和数组,基本类型它接不住。更关键的是,它一旦被整体替换,响应性说没就没。比如下面这种写法:
const state = reactive({ count: 0 })
// 这么写,原来的 Proxy 引用就被换掉了
state = { count: 1 }
当然,正常写代码不会这么直接赋值,因为 const 也不允许。但实际场景里很容易出现类似问题:
let form = reactive({ name: '', age: 0 })
function reset() {
form = { name: '', age: 0 }
}
模板里还用着旧的 form,你这边已经换了一个新对象,页面不动,控制台也不报错。别问我怎么知道的。
所以我现在的习惯是:只要这个对象有可能被整体替换,就用 ref。 ref 包对象虽然脚本里要多写 .value,但它把“整体替换”和“改内部属性”这两件事都接住了:
const form = ref({ name: '', age: 0 })
form.value = { name: '张三', age: 18 }
form.value.name = '李四'
模板里还是 form.name,不用改。麻烦一点,但边界清楚。
为什么我后来大多数时候用 ref
组合式函数写多了会发现,返回 ref 比返回 reactive 更省心。比如一个简单的计数器:
function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
调用方解构也好,传递也好,都不容易丢响应性。你拿到的是 count 这个 ref 本身,不是某个 Proxy 上被解构出来的普通值。
但如果是下面这样:
function useUser() {
const state = reactive({ name: '', age: 0 })
return { ...state }
}
看着没问题,实际上返回出去的是两个普通值,响应性已经断了。要用 toRefs 才行:
function useUser() {
const state = reactive({ name: '', age: 0 })
return { ...toRefs(state) }
}
所以不是 reactive 不能用,是它很容易在“解构、展开、传参”这些动作里悄悄丢掉响应性。ref 也不是没坑,但它至少把值包在一个对象里,你拿到引用本身,心里更有数。
reactive 真正舒服的场景
我也不是完全不用 reactive。有一类场景它确实舒服:一组相关字段固定放在一起,只改属性,不整体替换。
比如一个弹窗状态:
const dialog = reactive({
visible: false,
title: '',
loading: false
})
function openDialog(title) {
dialog.title = title
dialog.visible = true
}
function closeDialog() {
dialog.visible = false
dialog.loading = false
}
这种状态对象结构稳定,字段也不多,用 reactive 少写很多 .value,模板里也直观。再配合 toRefs 往外解构,问题不大。
但要注意,reactive 对象里的 ref 属性会自动解包,数组和集合里就不一定了。这个边界别记混。比如:
const state = reactive({
count: ref(0)
})
console.log(state.count) // 0,不是 ref
这看起来很方便,但也容易让人误以为所有嵌套 ref 都这样。数组里的 ref、Map 里的 ref,该 .value 还是得 .value。我现在的做法是尽量别把 ref 和 reactive 混着塞来塞去,除非我很清楚这里需要自动解包。
解构是最大的分界线
reactive 最典型的坑就是解构:
const state = reactive({ count: 0 })
const { count } = state
count++ // 页面不动
因为 count 已经是普通数字了。解法要么别解构,要么用 toRefs:
const { count } = toRefs(state)
count.value++
ref 也有类似问题,但更直观:
const count = ref(0)
const { value } = count
value++ // 页面不动
你看,ref 解构的是 .value,普通值,一样丢。区别在于 ref 的 .value 太显眼了,写的时候会警觉;reactive 的属性访问太自然,反而容易忘。
所以我现在判断边界很简单:只要我打算解构,就先想清楚解构出来的是不是 ref。不是 ref,就别指望它单独响应。
整体替换对象时,ref 更省心
接口回填、表单重置、列表刷新,这些场景都容易整体替换对象。用 reactive 不是不能做,但得用 Object.assign 或者逐个字段改:
const form = reactive({ name: '', age: 0 })
// 可以
Object.assign(form, { name: '张三', age: 18 })
// 不建议,容易把响应性搞丢
form = { name: '张三', age: 18 }
用 ref 就没这个心理负担:
const form = ref({ name: '', age: 0 })
form.value = { name: '张三', age: 18 }
模板里 form.name 照常工作。唯一的代价是脚本里多写 .value。我现在的取舍是:状态对象如果存在“整体换掉”的可能,直接上 ref,别为了少写几个 .value 给自己埋雷。
模板自动解包别带进脚本
ref 在模板里会自动解包,这是 Vue 的贴心设计,但也容易让人在脚本里犯迷糊:
<script setup>
const count = ref(0)
count++ // 错,应该是 count.value++
</script>
<template>
{{ count }} <!-- 这里不用 .value -->
</template>
reactive 在模板里直接属性访问,脚本里也是直接属性访问,看起来更统一。但它和 ref 的边界不一样:ref 在模板自动解包,reactive 在模板不解包一个“外层包装”,因为它本来就没有外层包装。
我的建议是别在模板里做复杂判断,脚本里老老实实写 .value。模板里少写 .value 是语法糖,不是让你在脚本里也忘了它。
组合式函数返回值怎么设计
我现在写组合式函数,返回值优先用 ref 和普通函数:
function usePagination() {
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const next = () => page.value++
return { page, pageSize, total, next }
}
调用方解构 { page, next } 也没问题,因为 page 本身是 ref。要是返回 reactive 对象,调用方一解构就很容易出问题。
如果确实想返回一组状态,又不想让调用方写太多 .value,那就返回 readonly 或者明确用 toRefs:
function useFilters() {
const state = reactive({
keyword: '',
status: ''
})
return {
...toRefs(state),
state
}
}
但说实话,除非团队有统一约定,我现在更倾向于直接返回 ref。统一比“少写 .value”重要。
数组和集合的边界
数组也是对象,reactive([]) 可以用,但整体替换数组时同样难受:
const list = reactive([])
list = [1, 2, 3] // 不行,响应性断了
用 ref 就直观很多:
const list = ref([])
list.value = [1, 2, 3]
Vue 3 对数组索引赋值是支持的,list.value[0] = 1 也能触发更新。但如果你经常整体替换列表,ref 明显更稳。
Map、Set 这类集合,reactive 支持,但用起来限制不少。我的习惯是:如果只是内部改改,reactive 可以;如果要整体换、要跨函数传,还是 ref 包一层更省心。
我现在的实际选择清单
写业务代码时,我基本按下面这个清单选:
- 基本类型、字符串、数字、布尔值:
ref,没得选。 - 可能整体替换的对象:
ref,别犹豫。 - 固定表单、弹窗状态、一组稳定字段:
reactive,配合toRefs解构。 - 需要跨函数、跨模块传递的响应式状态:
ref,引用清楚。 - 只想浅响应:
shallowRef,别用深响应给自己找性能麻烦。 - DOM 或组件实例:
ref,但命名上跟数据 ref 区分开,比如inputRef、formRef。 - 从
reactive对象往外解构:先问自己是不是该用toRefs。
这些规则不是教条,但能避开大部分“页面为什么不更新”的问题。真遇到不更新,先查三件事:是不是解构了普通值,是不是整体替换了 reactive,是不是忘了 .value。这三类问题占了我在组合式 API 里踩坑的绝大多数。
边界不是教条,是让代码少出错
说到底,reactive 和 ref 不是谁替代谁的关系。reactive 适合结构稳定的对象状态,写起来顺手;ref 适合需要整体替换、需要跨函数传递、或者你不想在解构上动脑子的场景。
我现在不会为了“统一风格”强行全用 ref,也不会因为“不想写 .value”就把所有东西塞进 reactive。边界清楚比风格统一重要。代码是给人维护的,页面不更新的时候,能一眼看出问题在哪,比少写几个 .value 值多了。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。