Vue3项目里怎么精准获取DOM元素?和Vue2有啥不一样的坑要踩?
相信不少刚从Vue2转过来或者刚开始学Vue3的开发者,一开始写代码都会遇到和DOM操作相关的小麻烦:明明在Vue2里敲个$refs就能拿到的东西,到Vue3咋就不对了?获取到的元素有时候是null有时候是Proxy?动态生成的列表项DOM怎么单独拿到?今天就彻底捋清楚Vue3获取DOM的所有实用方法,顺便避避容易踩的坑。
Vue3获取DOM的核心方式——ref属性
不管是Vue2还是Vue3,官方都推荐尽量少直接操作DOM,但有些场景比如获取canvas画布、调用第三方UI库的原生API、设置复杂的动画触发点,还是绕不开的,Vue3里获取单个、静态的DOM元素,最基础的还是ref,但用法和Vue2比有了很大变化,本质原因是Vue3的响应式系统从Object.defineProperty换成了Proxy。
单个静态DOM的获取步骤
假设现在要给一个输入框自动聚焦,在Vue3 Composition API的setup语法糖或者setup函数里,得这么做:
- 先从vue包里解构出ref;
- 创建一个初始值为null的响应式变量,变量名要和后面DOM上绑的ref属性完全一致;
- 等组件挂载完成(onMounted生命周期钩子,因为Vue3的DOM也是异步渲染的,setup执行的时候页面结构还没完全生成),再去访问这个响应式变量的.value属性,拿到的才是真正的DOM元素;
- 拿到DOM后就能调用原生JS的方法了,比如focus()。
举个setup语法糖的例子,代码非常简洁:
<template>
<!-- 注意ref属性值和下面的inputRef完全一致 -->
<input type="text" ref="inputRef" placeholder="自动聚焦哦">
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 变量名必须和模板ref属性值匹配
const inputRef = ref(null)
// 必须等onMounted,setup阶段inputRef.value还是null
onMounted(() => {
inputRef.value?.focus()
})
</script>
这里加个可选链操作符?.是好习惯,万一模板里的ref属性写错了或者DOM没来得及渲染完,不会直接报错打断程序。
单个动态生成的DOM(比如v-if控制的元素)
如果DOM是用v-if或者v-show切换显示隐藏的,v-show还好,只是样式隐藏,DOM一直存在,只要是在onMounted之后或者切换显示后调用,基本能拿到;但v-if不一样,切换false的时候DOM会被销毁,切换true的时候才会重新创建,这时候直接在点击显示的事件里访问ref.value大概率还是null,因为Vue的DOM更新是批量异步的。
这时候就得用到nextTick了:等下一次DOM更新循环结束之后再执行回调,就能保证拿到新创建的DOM,比如做个点击按钮显示搜索框并自动聚焦的功能:
<template>
<button @click="toggleSearch">打开/关闭搜索</button>
<input
v-if="isShowSearch"
type="text"
ref="searchInputRef"
placeholder="搜点啥"
>
</template>
<script setup>
import { ref, nextTick, onMounted } from 'vue'
const isShowSearch = ref(false)
const searchInputRef = ref(null)
const toggleSearch = async () => {
isShowSearch.value = !isShowSearch.value
// 如果是打开搜索,等DOM更新完再聚焦
if (isShowSearch.value) {
await nextTick()
searchInputRef.value?.focus()
}
}
</script>
这里用async/await包装nextTick比用.then回调更直观,当然两种写法都可以。
获取多个动态生成的DOM元素——模板引用数组
这是Vue3比Vue2好用很多的地方!在Vue2里,要获取v-for生成的多个DOM,得给每个元素绑不同的ref或者用索引动态生成ref,特别麻烦;Vue3里直接给v-for的每个元素绑同一个字符串ref属性,然后对应的响应式变量就会变成一个DOM元素数组(注意是普通数组,不是响应式数组哦,更新的时候需要重新赋值或者手动触发更新,不过大部分场景下只需要读取就行)。
比如现在要给v-for渲染的10个div绑定点击事件,点击哪个就把哪个的背景色改成红色:
<template>
<div
v-for="(item, index) in 10"
:key="index"
ref="itemDivRef"
@click="changeBg(index)"
style="width: 100px; height: 100px; border: 1px solid #ccc; margin: 10px 0; cursor: pointer;"
>
{{ index + 1 }}
</div>
</template>
<script setup>
import { ref } from 'vue'
// 这里初始值也是null,等渲染完成会自动变成包含10个DOM的普通数组
const itemDivRef = ref([])
const changeBg = (targetIndex) => {
// 遍历数组或者直接用索引访问都行
itemDivRef.value.forEach((div, index) => {
div.style.backgroundColor = index === targetIndex ? '#ff4d4f' : '#fff'
})
}
</script>
这里有个小细节:数组里DOM的顺序和v-for的渲染顺序是一致的,所以用index直接访问完全没问题。
子组件的ref获取——拿到组件实例还是组件内部的DOM?
在Vue3里,给子组件绑ref,默认拿到的是子组件的Proxy实例,不是子组件的根DOM元素,这点和Vue2也不一样。
那如果要拿到子组件的根DOM怎么办?有两种方法:
- 子组件用defineExpose显式暴露自己的根DOM(推荐,符合Vue3的封装性原则,不会随便把子组件的所有属性和方法暴露给父组件);
- 父组件通过子组件实例的.$el属性获取(不推荐,因为.$el只在子组件挂载完成后才有值,而且如果子组件是多根组件(根元素不止一个),.$el会返回第一个根元素,不是很稳定)。
推荐方法:defineExpose显式暴露
举个例子,父组件要调用子组件内部一个canvas的toDataURL方法导出图片:
<!-- 子组件 CanvasGenerator.vue -->
<template>
<canvas ref="canvasRef" width="400" height="300" style="border: 1px solid #333;"></canvas>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const canvasRef = ref(null)
// 子组件内部先画个简单的圆形
onMounted(() => {
const ctx = canvasRef.value.getContext('2d')
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.fillStyle = '#1890ff'
ctx.fill()
})
// 显式暴露canvasRef和导出图片的方法给父组件
defineExpose({
canvasRef,
exportImage: () => canvasRef.value.toDataURL('image/png')
})
</script>
<!-- 父组件 App.vue -->
<template>
<CanvasGenerator ref="canvasGeneratorRef" />
<button @click="handleExport">导出图片</button>
</template>
<script setup>
import { ref } from 'vue'
import CanvasGenerator from './CanvasGenerator.vue'
const canvasGeneratorRef = ref(null)
const handleExport = () => {
const imgUrl = canvasGeneratorRef.value?.exportImage()
if (imgUrl) {
// 这里可以做下载图片的操作
const link = document.createElement('a')
link.href = imgUrl
link.download = 'blue-circle.png'
link.click()
}
}
</script>
这里注意,如果子组件没有用defineExpose暴露任何东西,父组件拿到的子组件实例里是找不到子组件内部的任何属性和方法的,完全是封装好的。
Vue3获取DOM的常见坑,你踩过几个?里其实已经提到了一些,但为了加深印象,再单独列出来避坑:
- setup阶段直接访问ref.value是null:这点一定要记住!Vue3的setup是在组件实例创建之前、DOM渲染之前执行的,此时不管是静态还是动态DOM,对应的ref.value都是初始值null;
- v-if控制的DOM没有用nextTick:刚才说过了,v-if切换true的时候DOM是异步创建的,必须等下一次DOM更新循环;
- 模板引用数组(v-for的ref)是普通数组,不是响应式的:如果v-for的列表项有增删,数组的长度和内容会自动更新,但如果想监听这个数组的变化,不能用watchEffect或者watch直接监听数组本身,因为它不是响应式的,得监听列表项的数据源(比如v-for绑定的那个数组),然后在回调里再访问模板引用数组;
- 多根组件的.$el不稳定:尽量不要用子组件实例的.$el,用defineExpose显式暴露需要的DOM或方法;
- 变量名和模板ref属性值不一致:这点很低级但很容易犯,比如变量名是input_ref,模板里是inputRef,肯定拿不到;
- 忘记用可选链操作符?.:如果DOM没渲染完或者ref属性写错了,直接访问.value会报错,加个?.能避免程序崩溃。
除了ref,还有没有其他获取DOM的方法?
虽然官方推荐用ref,但有些特殊场景用原生JS的方法也是可以的,不过要注意时机(同样要在onMounted之后或者nextTick之后):
- document.querySelector/document.querySelectorAll:可以用类名、ID、标签名等选择器获取,但如果是组件内部的DOM,最好加个限定前缀(比如用组件的根元素作为父元素,用parent.querySelector),避免选择到其他组件的同名元素;
- $parent/$children:哦不对,Vue3已经把$children移除了,$parent虽然还在,但也不推荐用,因为会破坏组件的单向数据流和封装性。
优先用ref,如果实在没办法再用原生JS的选择器,记得加限定前缀。
Vue3获取DOM的核心还是ref,但用法和Vue2有了很大变化:单个静态DOM用ref + onMounted;单个动态v-if控制的DOM用ref + nextTick;多个动态v-for生成的DOM用同一个字符串ref + 普通数组;子组件的DOM用defineExpose显式暴露,同时要避开常见的6个坑,尽量少直接操作DOM,能用Vue的响应式数据和指令解决的就不用原生JS。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



