深入剖析Ref响应式在Vue3中的实战应用与优化细节

迷人的镇逵 框架 阅读 2,498
赞 11 收藏
二维码
手机扫码查看
反馈

我的 Ref 写法,亲测靠谱

我用 Vue 3 的 ref 已经两年多了,从最开始觉得 reactive 更高级,到后来发现 ref 才是真香,中间踩过一堆坑。今天不讲概念,就聊实战中怎么写 ref 最稳,哪些写法看着没问题,上线就出事。

深入剖析Ref响应式在Vue3中的实战应用与优化细节

先上我现在的标准写法:

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

export default {
  setup() {
    const count = ref(0)
    const userInfo = ref(null)
    const loading = ref(false)

    const fetchUser = async () => {
      loading.value = true
      try {
        const res = await fetch('https://jztheme.com/api/user')
        userInfo.value = await res.json()
      } catch (err) {
        console.error('获取用户失败', err)
      } finally {
        loading.value = false
      }
    }

    onMounted(() => {
      fetchUser()
    })

    watch(count, (newVal) => {
      console.log('count 变了:', newVal)
    })

    return {
      count,
      userInfo,
      loading,
      fetchUser
    }
  }
}

为什么这么写?几个关键点:

  • 所有响应式数据都用 ref,哪怕是个对象。别搞什么 reactive 和 ref 混着用那一套,维护起来头疼。
  • null 初始值很关键。特别是接口返回的对象,设成 null 比 {} 好,模板里判断 userInfo.value 是否为真更安全。
  • 异步操作必须 try-catch。别信接口永远成功,网络超时、401、500 都可能,loading 卡住你都不知道为啥。
  • watch 显式监听 ref.value。Vue 3 自动解包,但显式写出来更清晰,团队新人也看得懂。

这套写法在我们项目里跑了十几个模块,没出过响应式失效的问题。下面说说那些我踩过的坑。

这几种错误写法,别再用了

第一种:直接解构 ref,完蛋。

const user = ref({ name: '张三', age: 18 })

// 错误!丢失响应性
const { name, age } = user.value

// 模板里用 {{ name }},改了不会更新

你以为解构出来的变量还能响应?想多了。ref 的响应性靠的是 .value 的 getter/setter,一解构就断了。正确做法是保留原 ref,或者用 toRefs(但只适合返回给 template)。

第二种:函数里新建 ref,每次调用都 new 一个,结果状态对不上。

const getUserStatus = () => {
  const status = ref('pending') // 每次调用都是新的 ref
  // ... 异步逻辑
  return status
}

// 多个组件调用,每个都有自己的 status,根本没法共享

这种写法常见于工具函数里滥用 ref。记住:ref 是状态容器,不是普通变量。要共享状态,就得把 ref 定义在函数外,或者用 provide/inject。

第三种:异步赋值时不注意作用域,this 或箭头函数搞混。

setup() {
  const data = ref([])

  setTimeout(function () {
    // 这里 this 不指向 setup 上下文
    // data.value.push 可能报错
  }, 1000)

  // 正确:用箭头函数
  setTimeout(() => {
    data.value = [1, 2, 3] // 没问题
  }, 1000)
}

看似低级,但我真见过有人因为用了 function 而导致 data 未定义,调试半天才发现是 this 问题。

第四种:ref 当布尔值用,却忘了初始化。

const showPanel = ref() // undefined!

// 模板里 v-if="showPanel" 一开始就是 false,但开发者以为是 false 是初始值
// 实际是 undefined,容易误导

建议明确写 ref(false),别省那两个字母。代码可读性比少打几个字重要得多。

实际项目中的坑

有个真实案例:我们做后台管理列表页,搜索条件用 ref 存,结果页面切换回来,条件丢了。

排查半天发现:每次路由进入都重新 setup,ref 重置了。解决办法有两个:

  • 用 keep-alive 缓存组件实例
  • 或者把搜索条件提到父级或 store 里

最后我们选了前者,简单粗暴。但要注意内存占用,别啥都缓存。

另一个问题是 ref 在 v-for 里的使用。比如你要高亮某个 item:

const activeIndex = ref(-1)

// 模板
// <div v-for="(item, index) in list" :class="{ active: index === activeIndex.value }">
//   <span @click="activeIndex.value = index">{{ item.name }}</span>
// </div>

这写法没问题,但如果 list 是动态的,index 可能变。更稳妥的做法是用唯一 key,比如 id:

const activeId = ref(null)

// <div v-for="item in list" :class="{ active: item.id === activeId.value }">
//   <span @click="activeId.value = item.id">{{ item.name }}</span>
// </div>

别小看这个细节,列表删增时 index 会漂移,用 id 才是正道。

还有个隐藏坑:ref 传给子组件,子组件改了父组件的值。

// 父组件
const msg = ref('hello')

// 传给子组件
<Child :msg="msg" />

// 子组件如果直接改 msg.value,虽然能改,但违反单向数据流
// 正确做法是 emit 事件,让父组件自己改

我们项目早期有人图省事直接改,结果多个子组件互相改,状态乱成一锅粥。后来统一规范:props 只读,改要通过事件。

一些实用技巧

1. ref 的默认值尽量具体。比如数组别用 ref(null),用 ref([]),避免模板里判空麻烦。

2. 复杂对象初始化,可以封装个函数:

const createEmptyUser = () => ({
  id: null,
  name: '',
  email: '',
  profile: { avatar: '', bio: '' }
})

const user = ref(createEmptyUser())

这样比写一堆默认字段清晰,还能复用。

3. 调试时,console.log(ref) 看不到值?记得加 .value:

console.log(count.value) // 别直接 log count

新手常犯这错,log 出来是个带 value 的对象,一脸懵。

4. ref 类型推导,TypeScript 用户注意:

const count = ref<number>(0)
const items = ref<string[]>([])

显式标注类型,避免 any 泛滥。配合 VSCode 的类型提示,开发体验提升一大截。

结语

以上是我这两年用 ref 总结下来的最佳实践。没有银弹,这套写法也不是完美的——比如 ref.value 写多了确实啰嗦,但换来的是稳定和可维护性。

我们项目目前 50+ 个页面都按这个模式走,协作起来没扯皮。比早期各种 reactive、ref 混用强太多了。

如果你有更好的方案,比如用 custom ref 或其他模式,欢迎评论区交流。毕竟前端这行,谁都不是真理掌握者,折腾完了能跑就行。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论