Vue3里怎么用ref获取DOM?实际场景咋玩得转?
不少刚接触Vue3的同学,在想通过ref获取DOM元素时总犯迷糊——代码写了,咋拿不到元素?ref和DOM绑定到底有啥门道?今天咱就把Vue3里ref操作DOM的事儿拆碎了讲,从基础用法到实际场景,再到避坑技巧,帮你把这块儿吃透。
Vue3里ref绑定DOM的基本逻辑是啥?
首先得明白,Vue3的ref
既是创建响应式数据的工具,也能用来“标记”DOM元素,简单说,就是在模板里给DOM加个ref
属性,和setup中定义的ref
变量对应上,就能拿到真实DOM了。
举个最基础的例子:
<template> <div ref="boxRef">我是要被获取的盒子</div> </template> <script setup> import { ref, onMounted } from 'vue' const boxRef = ref(null) // 先初始化为null,因为DOM还没挂载呢 onMounted(() => { console.log(boxRef.value) // 等DOM挂载完,这里就能拿到div的DOM元素啦 }) </script>
这里有两个关键点:一是生命周期时机——DOM得等组件挂载(onMounted
阶段)后才存在,所以提前在onBeforeMount
里访问,拿到的肯定是null
;二是访问方式——用ref
创建的变量,得通过.value
才能拿到实际值(DOM元素)。
和reactive处理DOM有啥不一样?
有些同学会问:用reactive
也能存DOM啊,为啥非得用ref
?这得从设计意图说起。
reactive
主打“对象的响应式”,适合处理复杂对象的属性联动;而ref
更像“引用容器”,专门用来包裹单个值(比如基本类型、DOM元素),举个对比例子:
<!-- 用reactive的写法 --> <template> <div ref="box">...</div> </template> <script setup> import { reactive, onMounted } from 'vue' const state = reactive({ box: null }) onMounted(() => { console.log(state.box) // 得通过对象属性访问 }) </script> <!-- 用ref的写法 --> <template> <div ref="boxRef">...</div> </template> <script setup> import { ref, onMounted } from 'vue' const boxRef = ref(null) onMounted(() => { console.log(boxRef.value) // 直接.value访问,更简洁 }) </script>
实际开发里,处理单个DOM元素时,ref
更轻便;要是想把DOM和其他响应式数据打包管理,reactive
才更合适,但90%的DOM引用场景,用ref
就够了。
列表循环里怎么用ref获取多个DOM?
如果是v-for
渲染的列表,想给每个项加ref,直接写ref="xxx"
可不行——因为循环里每个元素都要单独存DOM,得用“函数式ref”来收集。
看例子:假设要渲染水果列表,每个<li>
都要拿DOM:
<template> <ul> <li v-for="(item, index) in list" :key="index" :ref="setItemRef"> {{ item }} </li> </ul> </template> <script setup> import { ref, onMounted } from 'vue' const list = ref(['苹果', '香蕉', '橘子']) const itemRefs = ref([]) // 用来存所有li的DOM const setItemRef = (el) => { if (el) { // 元素创建时el存在,销毁时el是null,所以要判断 itemRefs.value.push(el) } } onMounted(() => { console.log(itemRefs.value) // 这里就是所有li的DOM组成的数组 }) </script>
原理是:v-for
循环时,每个<li>
渲染后会触发setItemRef
函数,把当前DOM元素(el
)传进去,我们在函数里把有效的el
塞进数组,就能批量拿到循环里的DOM了。注意:如果列表数据变化(比如增删元素),setItemRef
会重新执行,旧元素销毁时传入的el
是null
,所以要加if (el)
避免重复存或存错。
ref获取DOM后能做啥实际功能?
光会拿DOM不算本事,得知道在项目里咋用,分享几个高频场景,看完就懂“学这玩意儿有啥用”。
场景1:输入框自动聚焦
登录页、搜索框页面,希望用户进来直接能输入,不用点输入框,用ref就能实现:
<template> <input ref="inputRef" type="text" placeholder="请输入账号"> </template> <script setup> import { ref, onMounted } from 'vue' const inputRef = ref(null) onMounted(() => { inputRef.value.focus() // 页面加载完,输入框自动获得焦点 }) </script>
场景2:动态计算元素尺寸
做轮播图、自适应布局时,经常需要知道容器宽度,比如轮播图容器:
<template> <div ref="swiperRef" class="swiper">...</div> </template> <script setup> import { ref, onMounted } from 'vue' const swiperRef = ref(null) onMounted(() => { const width = swiperRef.value.offsetWidth console.log('轮播容器宽度:', width) // 根据宽度调整轮播逻辑 }) </script>
场景3:第三方库初始化
很多JS库(比如Chart.js、地图库)需要传入DOM元素才能初始化,以Chart.js为例,给canvas加ref:
<template> <canvas ref="chartRef" width="400" height="400"></canvas> </template> <script setup> import { ref, onMounted } from 'vue' import Chart from 'chart.js' const chartRef = ref(null) onMounted(() => { new Chart(chartRef.value, { type: 'bar', data: { labels: ['A', 'B', 'C'], datasets: [...] }, options: { ... } }) }) </script>
这些场景覆盖了“交互优化”“布局计算”“第三方集成”三大方向,掌握后能解决很多实际开发难题。
常见坑点和解决办法
学的时候顺风顺水,一到项目就栽跟头?这几个坑提前避掉。
坑1:打印ref.value是null,拿不到DOM
原因很简单:访问时机不对,DOM还没挂载就去拿,自然是null,比如在onBeforeMount
里写:
<script setup> import { ref, onBeforeMount } from 'vue' const boxRef = ref(null) onBeforeMount(() => { console.log(boxRef.value) // 这里DOM没挂载,输出null }) </script>
解决:把操作DOM的代码移到onMounted
或更晚的生命周期(比如onUpdated
)里。
坑2:想拿子组件里的DOM,结果拿的是组件实例
如果ref绑定的是子组件,默认拿到的是子组件的实例(不是子组件内部的DOM),比如子组件里有个按钮,父组件想拿按钮的DOM:
<!-- 子组件Child.vue --> <template> <button ref="btnRef">点我</button> </template> <script setup> import { ref } from 'vue' const btnRef = ref(null) </script> <!-- 父组件 --> <template> <Child ref="childRef" /> </template> <script setup> import { ref, onMounted } from 'vue' const childRef = ref(null) onMounted(() => { console.log(childRef.value) // 这里拿到的是子组件实例,不是按钮DOM }) </script>
解决:子组件用defineExpose
把内部的DOM ref暴露出来:
<!-- 子组件Child.vue 修改后 --> <template> <button ref="btnRef">点我</button> </template> <script setup> import { ref, defineExpose } from 'vue' const btnRef = ref(null) defineExpose({ btnRef }) // 把btnRef暴露给父组件 </script> <!-- 父组件 修改后 --> <template> <Child ref="childRef" /> </template> <script setup> import { ref, onMounted } from 'vue' const childRef = ref(null) onMounted(() => { console.log(childRef.value.btnRef) // 现在能拿到按钮的DOM啦 }) </script>
坑3:v-show和v-if搞混,ref拿错值
v-show
是控制元素的display
属性,元素始终在DOM里,所以ref能拿到;但v-if
是“销毁/重建”元素,当v-if="false"
时,元素被销毁,ref就是null
。
<template> <div v-show="isShow" ref="boxRef">v-show控制的元素</div> <div v-if="isShow" ref="boxRef2">v-if控制的元素</div> <button @click="toggle">切换显示</button> </template> <script setup> import { ref } from 'vue' const isShow = ref(true) const boxRef = ref(null) const boxRef2 = ref(null) const toggle = () => { isShow.value = !isShow.value console.log(boxRef.value) // v-show的元素,切换后still有值 console.log(boxRef2.value) // v-if的元素,false时变成null } </script>
所以做条件渲染时,得清楚用的是v-show还是v-if,避免ref突然变成null导致报错。
坑4:动态组件里的ref咋处理?
动态组件(<component :is="xxx" />
)的ref逻辑和普通组件一样:如果渲染的是子组件,默认拿到的是子组件实例;如果要拿子组件内部DOM,子组件得用defineExpose
暴露。
例子:根据类型渲染不同组件,拿内部DOM:
<template> <component :is="componentType" ref="compRef" /> </template> <script setup> import { ref, onMounted } from 'vue' import CompA from './CompA.vue' import CompB from './CompB.vue' const componentType = ref(CompA) const compRef = ref(null) onMounted(() => { // 如果CompA用defineExpose暴露了内部DOM,这里就能拿到 console.log(compRef.value.innerRef) }) </script>
核心还是“子组件主动暴露+父组件在合适时机访问”。
Vue3 ref DOM在性能上有啥要注意的?
虽然ref操作DOM很方便,但频繁操作(比如窗口resize时一直获取元素尺寸)可能影响性能,分享两个优化思路:
节流防抖,减少不必要的DOM访问
比如监听窗口resize,每次resize都去拿DOM尺寸,会触发大量计算,用节流函数限制频率:
<template> <div ref="boxRef">...</div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import { throttle } from 'lodash-es' // 引入节流函数 const boxRef = ref(null) const handleResize = throttle(() => { const width = boxRef.value.offsetWidth // 处理宽度变化逻辑 }, 200) // 每200毫秒执行一次 onMounted(() => { window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) }) </script>
结合响应式,避免过度操作DOM
Vue的优势是数据驱动视图,尽量用数据变化带动DOM更新,而不是直接操作DOM,比如要改变元素样式,优先用class
或style
的响应式绑定,而非直接用ref修改el.style
。
反例(不推荐):
<template> <div ref="boxRef">...</div> <button @click="changeColor">变色</button> </template> <script setup> import { ref } from 'vue' const boxRef = ref(null) const changeColor = () => { boxRef.value.style.backgroundColor = 'red' // 直接操作DOM,脱离响应式 } </script>
正例(推荐):
<template> <div :style="{ backgroundColor: bgColor }">...</div> <button @click="changeColor">变色</button> </template> <script setup> import { ref } from 'vue' const bgColor = ref('white') const changeColor = () => { bgColor.value = 'red' // 数据驱动,Vue自动更新DOM } </script>
ref用来“引用”DOM没问题,但频繁直接操作DOM容易踩性能坑,得结合Vue的响应式机制和性能优化手段。
绕了这么一大圈,从基础用法到实际场景,再到避坑和性能,把Vue3里ref操作DOM的事儿讲透了吧?记住核心逻辑:ref
是DOM的“抓手”,要抓准时机(mounted后)、用对方式(.value访问)、避开关联坑点(比如子组件暴露、v-if/v-show区别),下次遇到DOM操作需求,就知道咋用ref优雅解决啦~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。