Code前端首页关于Code前端联系我们

ref到底是干啥的?

terry 2周前 (10-01) 阅读数 35 #Vue
文章标签 ref 作用

p>刚接触Vue3的同学,肯定对ref这个API又好奇又有点懵——它和reactive有啥区别?啥时候该用ref?怎么给DOM元素绑定ref?这些问题是不是天天在你脑子里打转?今天咱就用最接地气的方式,把ref的用法掰碎了讲清楚,看完你就明白啥场景用它、怎么用更顺手~


你可以把ref理解成「给数据穿了件响应式的外衣」,不管是字符串、数字这些基本类型,还是对象、数组这些引用类型,用ref包一层,就能让它们在Vue里变成「改了能被检测到,页面自动更新」的响应式数据。

举个例子:

<script setup>
import { ref } from 'vue'
// 基本类型:数字
const count = ref(0)  
// 引用类型:对象
const user = ref({ name: '张三', age: 18 })  
</script>  
<template>
  <!-- 模板里不用写.value,Vue自动帮你解包 -->
  <button @click="count++">{{ count }}</button>  
  <p>{{ user.name }} 今年 {{ user.age }} 岁</p>  
</template>

这里count是数字(基本类型),user是对象(引用类型),用ref包完后,修改count.value或者user.value.name,页面都会自动更新。

那和reactive有啥区别?简单说:reactive只能包引用类型(对象/数组),而且对基本类型无效;但ref更灵活,基本类型、引用类型都能包,还能在不同组件/函数之间传递时保持响应式(后面讲场景会说到)。

什么时候该用ref,什么时候用reactive

很多同学分不清这俩,其实看数据类型+使用场景就行:

场景1:处理基本类型数据 → 必须用ref

是否显示弹窗」(布尔值)、「点赞数」(数字)、「用户昵称」(字符串)这些,因为reactive对基本类型没用(你用reactive包数字,改了页面不更新),所以这类场景只能用ref

场景2:想让数据「简单传递」 → 优先用ref

如果需要把响应式数据当参数传给函数,或者从自定义Hook里返回,用ref更稳,因为reactive返回的是Proxy对象,直接传递可能丢响应式(比如函数里修改参数,外层不一定能检测到);但ref是个「盒子」,传递的是盒子的引用,改value能被检测到。

举个自定义Hook的例子:

// useCount.js
import { ref } from 'vue'
export function useCount(initial) {
  const count = ref(initial)
  const increment = () => count.value++
  return { count, increment }
}
// 组件里用
<script setup>
import { useCount } from './useCount.js'
const { count, increment } = useCount(0)
</script>

这里把countref包的)返回出去,组件里用的时候响应式完全没问题,要是用reactive包对象,返回后外层修改可能踩坑。

场景3:对象结构复杂,想直接「点」属性 → 用reactive

比如用户信息有头像、姓名、地址、爱好等多层结构,用reactive({ avatar: '', name: '', address: { city: '' } }),直接写user.nameuser.value.name更顺手,这种情况选reactive

怎么给DOM元素加ref

有时候需要操作真实DOM(比如获取宽高、滚动位置、初始化第三方库),这时候ref能帮你拿到DOM节点,步骤很简单:

  1. 在setup里定义ref变量:用ref(null)初始化,因为DOM还没渲染,初始是null
  2. 模板里给DOM加ref属性:属性名和定义的变量名一致。
  3. 在组件挂载后操作DOM:因为DOM要等组件渲染完才存在,所以要在onMounted钩子(或之后的生命周期)里访问ref.value

举个「获取div宽度」的例子:

<script setup>
import { ref, onMounted } from 'vue'
// 1. 定义ref变量
const divRef = ref(null)  
onMounted(() => {
  // 3. 组件挂载后,divRef.value就是真实DOM
  console.log(divRef.value.offsetWidth) // 打印div的宽度
})
</script>  
<template>
  <!-- 2. 模板里绑定ref -->
  <div ref="divRef" style="width: 200px; height: 100px; background: pink"></div>  
</template>

注意哦!如果是循环渲染的列表(v-for),想给每个item加ref,得用数组形式的ref,比如const listRefs = ref([]),模板里ref="(el) => listRefs.push(el)",这样每个DOM会被收集到数组里~

修改ref的响应式数据要注意啥?

改数据的时候,最容易懵的是什么时候要写.value,什么时候不用

情况1:基本类型 → 必须通过.value修改

比如const count = ref(0),如果想让count从0变1,得写count.value = 1,要是直接写count = 1,这就把count从「ref包的响应式对象」变成普通数字了,响应式直接丢了,页面也不会更新!

情况2:引用类型(对象/数组)→ 改属性或元素不用额外操作

比如const user = ref({ name: '张三' }),修改user.value.name = '李四',页面会更新;如果是数组const list = ref([1,2,3]),执行list.value.push(4)或者list.value[0] = 0,也会触发更新。

但如果是整个替换引用类型,比如user.value = { name: '王五' },这时候也能触发更新,因为refvalue本身是响应式的(内部其实用reactive包了一层),替换整个对象相当于改了value的引用,Vue能检测到~

refcomputedwatch怎么配合?

实际开发中,ref经常和计算属性、侦听器一起用,这里讲最常用的场景:

computed配合

计算属性依赖ref的值时,直接用ref.value就行:

<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// 计算count的两倍
const double = computed(() => count.value * 2)  
</script>  
<template>
  <button @click="count++">{{ count }}</button>  
  <p>两倍是:{{ double }}</p>  
</template>

computed会自动跟踪count.value的变化,count变了,double也会自动更新。

watch配合

侦听ref有两种写法,看你需不需要「深度侦听」:

  • 侦听基本类型的ref:直接传ref变量

    const count = ref(0)
    watch(count, (newVal, oldVal) => {
      console.log('count变了:', newVal, oldVal)
    })
  • 侦听引用类型的ref(对象/数组):如果要侦听内部属性变化,需要加{ deep: true }

    const user = ref({ name: '张三', info: { city: '北京' } })
    watch(user, (newVal) => {
      console.log('user内部变了:', newVal)
    }, { deep: true }) // 深度侦听,user里任何属性变了都触发
  • 只侦听ref的某个属性:可以用函数返回值的方式

    watch(() => user.value.name, (newName) => {
      console.log('名字变了:', newName)
    })

实际项目里ref的常见场景

光懂语法没用,得知道在哪用!分享几个工作中高频用到ref的场景:

场景1:基础状态管理

弹窗的显隐、按钮点击次数、加载状态这些「单个值」的状态,用ref最方便,比如做个弹窗组件:

<script setup>
import { ref } from 'vue'
const isShow = ref(false)
const openDialog = () => isShow.value = true
const closeDialog = () => isShow.value = false
</script>  
<template>
  <button @click="openDialog">打开弹窗</button>  
  <div v-if="isShow" class="dialog">
    <h3>我是弹窗</h3>
    <button @click="closeDialog">关闭</button>
  </div>
</template>

场景2:复杂逻辑抽离(自定义Hook)

把重复的逻辑抽到自定义Hook里,用ref管理状态,让组件更简洁,比如封装一个「请求数据并管理loading」的Hook:

// useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  const fetchData = async () => {
    loading.value = true
    try {
      const res = await fetch(url)
      data.value = await res.json()
      error.value = null
    } catch (e) {
      error.value = e
      data.value = null
    } finally {
      loading.value = false
    }
  }
  return { data, loading, error, fetchData }
}
// 组件里用
<script setup>
import { useFetch } from './useFetch.js'
const { data, loading, fetchData } = useFetch('https://api.example.com/data')
</script>

场景3:第三方库集成(比如ECharts)

很多第三方库需要操作DOM,这时候用ref存DOM容器,再初始化库,以ECharts为例:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
  // 组件挂载后,初始化图表
  chartInstance = echarts.init(chartRef.value)
  chartInstance.setOption({ /* 配置项 */ })
})
onUnmounted(() => {
  // 组件销毁前,销毁图表实例(避免内存泄漏)
  if (chartInstance) {
    chartInstance.dispose()
    chartInstance = null
  }
})
</script>  
<template>
  <div ref="chartRef" style="width: 600px; height: 400px"></div>  
</template>

场景4:父子组件通信(父拿子实例)

父组件想调用子组件的方法或访问子组件的状态,子组件用defineExpose暴露,父组件用ref获取子组件实例。

子组件:

<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// 暴露给父组件
defineExpose({ count, increment })
</script>

父组件:

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const handleClick = () => {
  // 调用子组件的方法
  childRef.value.increment()
  // 访问子组件的状态
  console.log(childRef.value.count)
}
</script>  
<template>
  <Child ref="childRef" />  
  <button @click="handleClick">让子组件计数+1</button>  
</template>

最后总结一下,ref是Vue3里超灵活的响应式工具,不管是基本类型、引用类型,还是操作DOM、抽离逻辑,都能hold住,核心记住:基本类型改值要带.value,模板里自动解包不用写;引用类型改属性不用额外操作,传递响应式数据优先用ref更稳~多在项目里试试这些场景,你就会越来越顺手啦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门