在 Vue 开发中,虽然我们推崇“数据驱动视图”的理念,尽量避免直接操作 DOM,但在某些特定场景下(如集成第三方库、聚焦输入框、获取元素尺寸等),我们仍然需要直接访问 DOM 元素。
大多数开发者第一时间想到的就是
$refs。确实,它是 Vue 提供的官方标准方案。但是,你是否知道除了 $refs,还有其他几种优雅甚至更灵活的方式来获取和操作 DOM?本文将深入探讨 Vue(涵盖 Vue 2 和 Vue 3)中获取 DOM 元素的多种方法,帮助你根据具体场景选择最佳实践。
方法一:标准的 ref 属性(最推荐)
这是 Vue 官方首推的方式,适用于绝大多数场景。它的特点是响应式且安全,Vue 会确保在组件挂载完成后才填充 ref。
Vue 3 (Composition API)
在 Vue 3 的组合式 API 中,我们直接使用
ref() 函数创建一个同名变量。vue
编辑
1<template>
2 <div ref="myBox">我是一个盒子</div>
3 <button @click="logWidth">获取宽度</button>
4</template>
5
6<script setup>
7import { ref, onMounted } from 'vue';
8
9// 1. 创建一个同名的 ref 变量
10const myBox = ref(null);
11
12const logWidth = () => {
13 if (myBox.value) {
14 console.log('宽度:', myBox.value.offsetWidth);
15 }
16};
17
18// 确保在挂载后访问,虽然 ref 在模板渲染后会自动赋值,但涉及尺寸通常建议 onMounted
19onMounted(() => {
20 console.log('DOM 已就绪:', myBox.value);
21});
22</script>
Vue 2 / Vue 3 (Options API)
在选项式 API 中,通过
this.$refs 访问。javascript
编辑
1export default {
2 mounted() {
3 // this.$refs.myBox 即为 DOM 元素
4 console.log(this.$refs.myBox.offsetWidth);
5 }
6}
优点:
- 类型安全(配合 TypeScript 极佳)。
- 生命周期管理清晰,组件卸载后自动清理。
- 能够获取子组件实例(而不仅仅是 DOM)。
缺点:
- 需要在模板和脚本中维护相同的命名。
- 在
v-for中使用时,Vue 3 中会得到一个数组,Vue 2 中行为略有不同,处理稍显繁琐。
方法二:原生 JavaScript 选择器(document.querySelector)
既然 Vue 最终渲染的是标准 HTML,那么原生的 DOM API 当然依然可用。
javascript
编辑
1import { onMounted } from 'vue';
2
3onMounted(() => {
4 const element = document.querySelector('.my-custom-class');
5 if (element) {
6 console.log('通过类名获取:', element);
7 }
8});
适用场景:
- 操作非 Vue 管理的 DOM(如由第三方库动态插入的元素)。
- 快速调试或临时脚本。
⚠️ 风险与缺点:
- 破坏封装性:如果页面中有多个相同类名的元素,可能会选错。
- SSR 问题:在服务端渲染(SSR)时,
document对象不存在,直接调用会报错,必须包裹在onMounted或client-only逻辑中。 - 难以追踪:这种隐式的依赖关系让代码更难维护和重构。
建议:除非万不得已,否则不推荐在组件内部常规使用此方法。
方法三:自定义指令 (Custom Directives)
当你需要在 DOM 元素被插入页面时立即执行某些逻辑(如自动聚焦、初始化图表),自定义指令是比生命周期钩子更优雅的方案。指令的
mounted 钩子直接接收真实的 DOM 元素作为参数。javascript
编辑
1// directives/focus.js
2export default {
3 mounted(el) {
4 // el 就是绑定的 DOM 元素,无需任何额外获取步骤
5 el.focus();
6 console.log('元素已通过指令获取:', el);
7 }
8};
vue
编辑
1<template>
2 <!-- 直接使用指令 -->
3 <input v-focus type="text" />
4</template>
5
6<script setup>
7import focus from './directives/focus';
8</script>
优点:
- 逻辑复用:将 DOM 操作逻辑封装在指令中,可在多个组件复用。
- 时机精准:直接在元素挂载时触发,无需担心生命周期顺序。
- 代码解耦:模板中声明意图,脚本中无需编写获取 DOM 的代码。
缺点:
- 需要额外的文件注册和管理。
- 对于一次性简单操作,可能显得“杀鸡用牛刀”。
方法四:事件对象 ($event)
如果你获取 DOM 的目的是为了响应用户的交互(如点击、鼠标移动),那么事件处理函数中的
$event 对象直接包含了触发事件的 DOM 元素(event.target 或 event.currentTarget)。vue
编辑
1<template>
2 <button @click="handleClick">点击我</button>
3</template>
4
5<script setup>
6const handleClick = (event) => {
7 // event.currentTarget 是绑定监听器的元素
8 const btn = event.currentTarget;
9 console.log('按钮宽度:', btn.offsetWidth);
10
11 // event.target 是实际触发事件的元素(如果有子元素可能是子元素)
12 console.log('触发源:', event.target);
13};
14</script>
优点:
- 完全不需要预先定义 ref。
- 天然适配合交互相关的 DOM 操作。
- 性能最好,无额外开销。
局限:
- 仅适用于事件触发场景,无法在组件初始化时主动获取。
方法五:通过组件实例 $el (仅限根节点)
在 Vue 中,每个组件实例都有一个
$el 属性,指向该组件的根 DOM 元素。javascript
编辑
1// 在组件内部
2onMounted(() => {
3 console.log('根元素:', this.$el); // Options API
4 // 或 getCurrentInstance().proxy.$el (Composition API 不推荐直接这样用,通常结合 ref)
5});
注意:
- 它只能获取组件模板的最外层元素。
- 如果根元素是
<template>或多根节点(Vue 3 Fragments),$el的行为会有所不同(多根节点时为第一个节点)。 - 父组件获取子组件的 DOM:父组件可以通过
ref获取子组件实例,然后访问childComponentRef.value.$el。
方法六:v-for 中的函数式 Ref (进阶技巧)
在 Vue 3 中,
:ref 可以绑定一个函数。这在处理列表动态生成的 DOM 时非常有用,可以将所有 DOM 元素收集到一个数组或对象中,而不需要手动遍历 $refs。vue
编辑
1<template>
2 <div v-for="item in list" :key="item.id" :ref="setRef">
3 {{ item.name }}
4 </div>
5</template>
6
7<script setup>
8import { ref } from 'vue';
9
10const domList = ref([]);
11
12const setRef = (el) => {
13 if (el) {
14 domList.value.push(el);
15 }
16};
17
18// 此时 domList.value 就是一个包含所有对应 DOM 元素的数组
19</script>
优点:
- 完美解决
v-for中 ref 变成数组后难以对应索引的问题(你可以自定义存储结构,比如 Map)。 - 灵活性极高。
总结与最佳实践对比
表格
| 方法 | 适用场景 | 推荐指数 | 备注 |
|---|---|---|---|
ref 属性 |
通用场景,需频繁访问或操作特定元素 | ⭐⭐⭐⭐⭐ | 首选方案,类型友好,维护性强 |
事件对象 $event |
用户交互触发的 DOM 操作 | ⭐⭐⭐⭐⭐ | 最自然的事件处理方式 |
| 自定义指令 | 底层 DOM 操作逻辑复用(如聚焦、懒加载) | ⭐⭐⭐⭐ | 封装性好,适合通用工具 |
$el |
仅需访问组件根节点 | ⭐⭐⭐ | 局限性较大,常用于父子组件通信 |
| 函数式 Ref | v-for 列表中的复杂 DOM 收集 |
⭐⭐⭐⭐ | 解决列表引用痛点 |
| 原生 JS 选择器 | 第三方库集成、非 Vue 管控区域 | ⭐⭐ | 慎用,易导致耦合和 SSR 错误 |
💡 核心建议
- 首选
ref:在 90% 的情况下,ref都是最清晰、最安全的选择。 - 避免直接操作 DOM:如果可以通过数据绑定(
:class,:style,v-if)解决问题,就不要去摸 DOM。 - 注意生命周期:无论用哪种方法,务必确保在 DOM 渲染完成后(如
onMounted)再进行读取尺寸或位置的操作。 - SSR 兼容性:如果你的应用涉及服务端渲染,请绝对避免在 setup 顶层或
beforeMount中使用依赖window/document的方法(如原生选择器),务必放入onMounted。
掌握这些多样化的方法,能让你的 Vue 代码更加灵活、健壮,也能在面对复杂需求时游刃有余。下次需要操作 DOM 时,不妨想想除了
$refs,是否还有更适合当前场景的“利器”?