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

vue3里怎么用ref获取DOM元素?比Vue2的$refs好用在哪?

terry 43分钟前 阅读数 16 #Vue

刚从Vue2转过来或者刚开始学Vue3的朋友,是不是经常会在查资料的时候看到“获取DOM别用document.getElementById了,用ref!”这种话?但翻到Vue3的ref用法,发现不是直接挂在$refs上,又懵了一会儿?别慌,今天咱们就把Vue3 ref获取DOM的所有细节掰扯清楚,包括基础操作、进阶场景、和Vue2的本质区别,还有踩过的小坑,看完你肯定能顺手用起来。

基础操作:怎么把ref和DOM绑起来

在Vue3里,绑定ref的步骤比Vue2稍微多了一点点,但逻辑更清晰,都是显式声明的,不会像Vue2那样有时候怕$refs的属性名写错或者被覆盖。

首先第一步,你得在script setup(推荐的Composition API写法,简洁)或者setup函数里用ref函数声明一个空的响应式变量,注意哦,获取DOM的ref初始值必须设为null或者undefined,因为组件挂载前DOM还不存在,设其他值没用还可能干扰初始化逻辑。 举个简单的小例子,比如我要获取一个输入框,让页面一加载就自动聚焦它:

<script setup>
import { ref, onMounted } from 'vue'
// 1. 声明初始值为null的响应式变量
const inputRef = ref(null)
// 3. 等组件挂载完,DOM生成了再操作
onMounted(() => {
  inputRef.value?.focus()
})
</script>
<template>
  <!-- 2. 在template里给目标DOM加ref属性,属性值和刚才声明的变量名完全一致 -->
  <input type="text" ref="inputRef" placeholder="页面加载后我会自动聚焦~">
</template>

这里有个关键点:在script setup里操作DOM的ref,必须加.value,因为ref返回的是一个RefImpl响应式对象,真实的DOM值藏在value属性里,还有,一定要用可选链操作符或者提前判断if (inputRef.value),不然如果DOM渲染失败(比如v-if条件暂时不满足),直接访问inputRef.value.focus()会报错“Cannot read properties of null”。

进阶场景1:循环生成的DOM列表怎么批量获取

在Vue2里,循环绑定ref会自动生成一个数组存对应的DOM,但在Vue3的Composition API里,默认不会自动聚合数组了,得自己写个小函数或者用计算属性?其实有两种常用且靠谱的方法,看你习惯哪种。

第一种方法是用函数类型的ref属性,就是给v-for里的DOM加的ref不是字符串,而是一个函数,这个函数会接收当前循环项对应的DOM作为第一个参数,循环索引作为第二个参数,我们可以手动把这些DOM推进一个普通数组或者响应式数组里。 比如要获取一组卡片,点击某个按钮给偶数位的卡片加个高亮:

<script setup>
import { ref } from 'vue'
const cardList = ref([{ id: 1, content: '卡片1' }, { id: 2, content: '卡片2' }, { id: 3, content: '卡片3' }, { id: 4, content: '卡片4' }])
const cardRefs = ref([]) // 用来存所有卡片DOM的响应式数组
// 手动存DOM的函数
const setCardRef = (el, index) => {
  if (el) {
    cardRefs.value[index] = el
  }
}
// 点击高亮偶数位卡片
const highlightEvenCards = () => {
  cardRefs.value.forEach((card, index) => {
    if (index % 2 === 1) {
      card.classList.add('highlight')
    } else {
      card.classList.remove('highlight')
    }
  })
}
</script>
<template>
  <div class="card-container">
    <!-- v-for里绑定函数类型的ref -->
    <div 
      v-for="(item, index) in cardList" 
      :key="item.id"
      class="card"
      :ref="setCardRef"
    >{{ item.content }}</div>
    <button @click="highlightEvenCards">高亮偶数位卡片</button>
  </div>
</template>
<style scoped>
.card { margin: 10px; padding: 20px; border: 1px solid #eee; border-radius: 4px; }
.highlight { background-color: #e6f7ff; border-color: #1890ff; }
</style>

这种方法的好处是可以自定义存储逻辑,比如根据循环项的id存成对象,而不是按索引存数组,当循环项删除、插入或者排序时,可能会更方便。

第二种方法是用Vue 3.2+新增的<script setup>结合<template ref="xxx">隐式获取?不对不对,隐式获取还是单个的,批量的话官方文档更推荐第一种,除非你用的是Options API(不过现在大部分新项目都用Composition API了),Options API里v-for绑定字符串ref还是会自动生成数组,但顺序可能和循环项的顺序不一致,尤其是有异步更新的时候,所以尽量还是用Composition API的函数ref。

进阶场景2:获取子组件的DOM或者方法

除了获取普通的DOM元素,Vue3的ref还能获取子组件的实例,然后通过实例访问子组件的方法或者DOM,不过这里有个注意点:如果子组件用的是<script setup>,默认是封闭的,父组件不能直接拿到子组件的任何属性或方法,除非子组件用defineExpose显式暴露出来。

先看父组件的部分,其实和获取普通DOM差不多,就是声明ref、绑定到子组件的ref属性、组件挂载后操作:

<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
  // 访问子组件暴露的方法
  childRef.value?.sayHello()
  // 访问子组件暴露的DOM元素
  console.log(childRef.value?.childInput)
})
</script>
<template>
  <Child ref="childRef" />
</template>

再看子组件的部分,必须显式暴露:

<!-- 子组件 Child.vue -->
<script setup>
import { ref, defineExpose } from 'vue'
// 子组件自己的方法
const sayHello = () => {
  alert('我是子组件的sayHello方法~')
}
// 子组件自己的DOM
const childInput = ref(null)
// 显式暴露给父组件
defineExpose({ sayHello, childInput })
</script>
<template>
  <input type="text" ref="childInput" placeholder="这是子组件的输入框">
</template>

如果子组件没用defineExpose,父组件打印childRef.value会发现是空对象,什么都访问不了,这个设计其实挺好的,符合封装的原则,不会随便把内部的东西暴露给父组件,减少了组件之间的耦合。

进阶场景3:v-if条件渲染的DOM怎么安全获取

刚才基础操作里提到了可选链,但如果v-if的条件是动态变化的,比如点击一个按钮才显示某个弹窗,这时候怎么确保弹窗的DOM生成后再操作它? 这里可以用Vue3的响应式特性,监听v-if绑定的布尔值的变化,当它变成true的时候,用nextTick函数延迟执行操作,nextTick的作用是等待下一次DOM更新循环结束后再执行回调,因为Vue的DOM更新是异步批量的,你修改了v-if的条件,DOM不会立刻生成,得等nextTick之后才会更新到页面上。 举个显示弹窗后自动聚焦关闭按钮的例子:

<script setup>
import { ref, watch, nextTick } from 'vue'
const showModal = ref(false)
const closeBtnRef = ref(null)
// 监听showModal的变化
watch(showModal, (newVal) => {
  if (newVal) {
    // 等DOM更新完(弹窗生成)再聚焦
    nextTick(() => {
      closeBtnRef.value?.focus()
    })
  }
})
</script>
<template>
  <button @click="showModal = true">打开弹窗</button>
  <div v-if="showModal" class="modal-mask">
    <div class="modal-content">
      <p>这是一个弹窗</p>
      <button ref="closeBtnRef" @click="showModal = false">关闭</button>
    </div>
  </div>
</template>
<style scoped>
.modal-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; }
.modal-content { background-color: white; padding: 20px; border-radius: 4px; }
</style>

这个场景里,nextTick是必不可少的,不然即使showModal变成true了,回调里的closeBtnRef.value还是null。

Vue3的ref获取DOM和Vue2的$refs到底有啥不一样?

很多从Vue2转过来的朋友一开始会觉得Vue3的ref麻烦,又是显式声明又是加.value,还要用defineExpose,那为啥Vue3要改呢?其实改了之后好处更多,更符合现代前端的开发习惯。

Vue3的ref是显式声明的,你在script里一眼就能看到哪些变量是用来获取DOM或者子组件的,而Vue2的$refs是隐式挂载的,有时候你不知道哪个组件或者DOM绑定了$refs,得翻半天template,代码多了维护起来很头疼。

Vue3的ref是响应式的,而Vue2的$refs不是响应式的,不过这里的响应式不是说你修改DOM的属性会触发Vue的响应式更新(DOM的属性本来就不归Vue管,除非你用v-bind绑定),而是说你可以把ref变量传给子组件作为props,或者在其他响应式的代码里使用它,Vue2的$refs就做不到这一点。

还有,Vue3的Composition API里ref和reactive这些响应式API是统一的,不管是存普通数据、存DOM还是存子组件实例,都用ref或者reactive,学习成本更低,逻辑也更连贯,而Vue2里存数据用data,存DOM和子组件用$refs,分开的两套逻辑。

刚才提到的子组件封闭性,Vue3的<script setup>默认封闭,必须显式暴露,这个减少了组件之间的耦合,让代码更健壮,而Vue2的子组件不管是用Options API还是(旧版的)setup函数,所有的属性和方法父组件都能通过$refs访问到,很容易造成意外的修改,维护起来很麻烦。

踩过的小坑,大家别再踩了

  1. 循环绑定字符串ref没用:刚才说过了,Vue3的Composition API里v-for绑定字符串ref不会自动生成数组,要用函数类型的ref。
  2. 组件挂载前操作DOM:一定要在onMounted或者nextTick之后操作DOM,不然DOM还没生成,访问value会报错。
  3. 不用可选链或者提前判断:虽然onMounted或者nextTick之后DOM大概率生成了,但如果有动态条件渲染或者异步加载组件的情况,还是可能出现null,所以用可选链或者提前判断更安全。
  4. 子组件没用defineExpose:父组件访问子组件的属性或方法之前,一定要确保子组件用defineExpose显式暴露了,不然会得到空对象。
  5. 用ref修改DOM属性,忽略了v-bind:如果某个DOM属性你需要频繁修改,或者需要和Vue的响应式数据关联起来,尽量用v-bind绑定,而不是直接用ref操作DOM,因为直接操作DOM会让Vue的响应式系统和实际DOM脱节,维护起来更麻烦,有些特殊情况必须直接操作DOM,比如输入框聚焦、滚动到某个位置、操作canvas这些,这时候用ref是没问题的。

总结一下

Vue3里用ref获取DOM或者子组件,步骤其实很简单:显式声明初始值为null的响应式变量,在template里绑定到对应的元素或组件上,等组件挂载完或者DOM更新完(用nextTick)之后加.value操作,和Vue2的$refs相比,Vue3的ref更显式、更统一、更符合封装原则,虽然稍微多了一点点代码,但逻辑更清晰,维护起来更方便。

最后再提醒大家一句:尽量不要滥用ref获取DOM,Vue的核心是数据驱动视图,能用v-bind、v-model这些指令解决的问题,就不要直接操作DOM,这样你的代码会更简洁、更健壮、更符合Vue的设计理念。

版权声明

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

热门