Tiana随笔

Vue3 中 ref 和 reactive 怎么选

· Vue

标签:#Vue #前端

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 就知道「这是个状态」。这比纠结选哪个更省精力。

相关文章