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>
这里把count
(ref
包的)返回出去,组件里用的时候响应式完全没问题,要是用reactive
包对象,返回后外层修改可能踩坑。
场景3:对象结构复杂,想直接「点」属性 → 用reactive
比如用户信息有头像、姓名、地址、爱好等多层结构,用reactive({ avatar: '', name: '', address: { city: '' } })
,直接写user.name
比user.value.name
更顺手,这种情况选reactive
。
怎么给DOM元素加ref
?
有时候需要操作真实DOM(比如获取宽高、滚动位置、初始化第三方库),这时候ref
能帮你拿到DOM节点,步骤很简单:
- 在setup里定义
ref
变量:用ref(null)
初始化,因为DOM还没渲染,初始是null
。 - 模板里给DOM加
ref
属性:属性名和定义的变量名一致。 - 在组件挂载后操作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: '王五' }
,这时候也能触发更新,因为ref
的value
本身是响应式的(内部其实用reactive
包了一层),替换整个对象相当于改了value
的引用,Vue能检测到~
ref
和computed
、watch
怎么配合?
实际开发中,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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。