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

Vue3里怎么用ref获取DOM?实际场景咋玩得转?

terry 2周前 (10-02) 阅读数 38 #Vue
文章标签 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会重新执行,旧元素销毁时传入的elnull,所以要加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,比如要改变元素样式,优先用classstyle的响应式绑定,而非直接用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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门