在Vue开发中,响应式系统和声明式渲染帮我们省去了大量手动操作DOM的麻烦——数据变化时,页面会自动同步更新,无需我们关心DOM的增删改查。但在实际开发中,总会遇到一些特殊场景:获取DOM元素的尺寸、手动触发DOM事件、集成第三方DOM库(如图表、富文本编辑器)等,这时就需要跳出“自动更新”的舒适区,手动操作DOM。
Vue为我们提供了两个“利器”来实现安全、高效的手动DOM操作:模板引用(Template Ref)用于精准获取模板中的DOM元素或子组件实例,生命周期钩子则用于在组件合适的阶段执行DOM操作,二者结合,既能避免DOM操作时机不当导致的报错,又能保证代码的可维护性。今天就来详细拆解这两个知识点,结合实战示例,带你掌握手动操作DOM的正确姿势。
模板引用:精准定位DOM元素的“坐标”
模板引用的核心作用,是给模板中的DOM元素或子组件打上“标记”,让我们能在脚本中直接访问到对应的实例。它的使用逻辑非常简洁,只需两步就能完成注册和访问,完全贴合Vue的组合式API风格。
1. 基础使用:注册与访问DOM元素
要使用模板引用,首先需要在模板中的目标元素上添加ref属性,给元素起一个唯一的“名称”,相当于给DOM元素设置了一个专属ID;然后在脚本中声明一个与该名称完全一致的ref变量,并初始化为null,这个变量就会成为访问该DOM元素的“入口”。
这里有一个关键注意点:脚本执行时,组件还未完成DOM挂载,此时模板中的元素还不存在,所以变量必须初始化为null,只有等组件挂载完成后,这个变量的.value才会指向对应的DOM元素。
<script setup>
import { ref } from 'vue';
// 声明与模板ref同名的变量,初始化为null
const titleRef = ref(null);
const inputRef = ref(null);
</script>
<template>
<!-- 给h2标签添加ref,名称为titleRef -->
<h2 ref="titleRef">Vue模板引用实战</h2>
<!-- 给输入框添加ref,名称为inputRef -->
<input
ref="inputRef"
type="text"
placeholder="请输入内容"
/>
</template>
2. 进阶用法:灵活控制引用的注册
除了直接使用字符串作为ref名称,我们还可以传递一个函数,实现对引用存储位置的完全控制。这种方式适合需要动态管理多个引用的场景,比如在列表中收集所有项的DOM元素。
示例:在v-for中使用函数式ref,收集所有列表项的DOM元素:
<script setup>
import { ref, onMounted } from 'vue';
// 用数组存储所有列表项的DOM元素
const itemRefs = ref([]);
const list = ref(['模板引用', '生命周期', 'DOM操作', 'Vue实战']);
// 函数式ref:收集每个列表项的DOM元素
const collectItemRef = (el) => {
// 避免添加null(如列表渲染前的空状态)
if (el) {
itemRefs.value.push(el);
}
};
onMounted(() => {
// 组件挂载后,可访问所有收集到的列表项DOM
console.log('所有列表项:', itemRefs.value);
// 给第一个列表项设置特殊样式
itemRefs.value[0].style.color = '#e53e3e';
itemRefs.value[0].style.fontWeight = 'bold';
});
</script>
<template>
<ul class="list">
<li
v-for="(item, index) in list"
:key="index"
:ref="collectItemRef"
>
{{ item }}
</li>
</ul>
</template>
<style>
.list {
padding: 0;
list-style: none;
}
.list li {
margin: 8px 0;
padding: 6px;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
3. 特殊场景:引用子组件实例
模板引用不仅可以用于普通DOM元素,还能用于子组件,此时引用变量会指向子组件的实例,让我们可以直接调用子组件的方法、访问子组件的响应式数据。需要注意的是,子组件必须通过defineExpose将需要暴露的方法和数据导出,否则父组件无法访问。
示例:父组件通过模板引用调用子组件方法:
<!-- 子组件 Counter.vue -->
<script setup>
import { ref } from 'vue';
// 子组件内部的响应式数据
const count = ref(0);
// 子组件的方法
const increment = () => {
count.value++;
};
// 暴露需要被父组件访问的属性和方法
defineExpose({
count,
increment
});
</script>
<template>
<p>子组件计数:{{ count }}</p>
</template>
<!-- 父组件 Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import Counter from './Counter.vue';
// 声明引用子组件的ref变量
const counterRef = ref(null);
onMounted(() => {
// 访问子组件的响应式数据
console.log('子组件初始计数:', counterRef.value.count);
// 调用子组件的方法
counterRef.value.increment();
console.log('调用子组件方法后计数:', counterRef.value.count);
});
</script>
<template>
<h3>父组件</h3>
<Counter ref="counterRef" />
</template>
生命周期钩子:把握DOM操作的“最佳时机”
有了模板引用,我们解决了“如何获取DOM”的问题,但还需要解决“何时操作DOM”的问题。Vue组件从创建到销毁,会经历一系列固定的阶段,比如初始化数据、编译模板、挂载DOM、更新DOM、销毁组件等,这些阶段被称为组件的生命周期。
生命周期钩子就是允许我们在组件特定阶段执行自定义代码的函数,通过这些钩子,我们可以精准控制DOM操作的时机,避免因DOM未挂载、已销毁等情况导致的报错。其中,最常用的三个钩子分别是onMounted、onUpdated和onUnmounted。
1. onMounted:组件挂载后执行(最常用)
onMounted钩子会在组件完成初始渲染、DOM元素完全挂载到页面后触发,此时模板引用已经可以正常访问,是执行DOM操作的最佳时机。无论是修改DOM样式、获取DOM尺寸,还是初始化第三方DOM库,都适合放在这个钩子中。
示例:组件挂载后,修改DOM文本、设置输入框自动聚焦:
<script setup>
import { ref, onMounted } from 'vue';
// 声明模板引用
const titleRef = ref(null);
const inputRef = ref(null);
// 注册onMounted钩子,组件挂载后执行
onMounted(() => {
// 修改h2标签的文本内容和样式
titleRef.value.textContent = '组件挂载完成,已更新标题';
titleRef.value.style.color = '#42b983';
titleRef.value.style.fontSize = '20px';
// 让输入框自动聚焦
inputRef.value.focus();
// 获取DOM元素的尺寸
const titleWidth = titleRef.value.offsetWidth;
console.log('标题宽度:', titleWidth + 'px');
});
</script>
<template>
<h2 ref="titleRef">初始标题</h2>
<input
ref="inputRef"
type="text"
placeholder="自动聚焦输入框"
/>
</template>
2. onUpdated:组件更新后执行
当组件的响应式数据发生变化,导致DOM重新渲染后,onUpdated钩子会被触发。这个钩子适合在DOM更新完成后,再次调整DOM样式或尺寸,比如根据数据变化动态修改元素位置、重新计算DOM尺寸等。
示例:数据更新后,动态修改DOM样式:
<script setup>
import { ref, onUpdated } from 'vue';
const count = ref(0);
const countRef = ref(null);
// 组件更新(count变化导致DOM更新)后执行
onUpdated(() => {
// 根据count的值,切换文本颜色
if (countRef.value) {
countRef.value.style.color = count.value % 2 === 0 ? '#e53e3e' : '#42b983';
}
});
</script>
<template>
<p ref="countRef">当前计数:{{ count }}</p>
<button @click="count++">计数+1</button>
</template>
3. onUnmounted:组件销毁前执行
onUnmounted钩子会在组件销毁前触发,主要用于清理手动创建的DOM资源,避免内存泄漏。比如移除手动添加的事件监听、销毁第三方DOM库实例、清空定时器等,这些操作如果不执行,会导致内存占用过高,影响页面性能。
示例:组件销毁前,清理事件监听和定时器:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const timeRef = ref(null);
let timer = null;
let resizeListener = null;
onMounted(() => {
// 启动定时器,更新时间
timer = setInterval(() => {
timeRef.value.textContent = new Date().toLocaleTimeString();
}, 1000);
// 添加窗口大小变化监听
resizeListener = () => {
console.log('窗口尺寸:', window.innerWidth + 'x' + window.innerHeight);
};
window.addEventListener('resize', resizeListener);
});
onUnmounted(() => {
// 组件销毁前,清理定时器和事件监听
clearInterval(timer);
window.removeEventListener('resize', resizeListener);
console.log('组件销毁,资源已清理');
});
</script>
<template>
<p ref="timeRef">{{ new Date().toLocaleTimeString() }}</p>
</template>
避坑指南:模板引用与生命周期的常见误区
虽然模板引用和生命周期钩子的用法不难,但在实际开发中,很多初学者会因为忽略细节而踩坑,这里总结几个最常见的误区,帮你避开陷阱。
误区1:在组件挂载前访问模板引用
模板引用只有在组件挂载后才能访问,若在脚本直接执行时(如setup函数中)访问ref.value,得到的永远是null,会导致DOM操作报错。
<!-- 错误示例 -->
<script setup>
import { ref } from 'vue';
const titleRef = ref(null);
// 错误:此时组件未挂载,titleRef.value为null
titleRef.value.textContent = '修改标题'; // 报错:Cannot set properties of null
</script>
正确做法:将DOM操作放在onMounted钩子中,确保组件已挂载。
误区2:异步注册生命周期钩子
生命周期钩子必须同步注册,不能在setTimeout、Promise.then等异步操作中注册,否则Vue无法关联到当前组件实例,钩子不会生效。
<!-- 错误示例 -->
<script setup>
import { onMounted } from 'vue';
// 错误:异步注册钩子,无法关联组件实例
setTimeout(() => {
onMounted(() => {
console.log('组件挂载'); // 不会执行
});
}, 1000);
</script>
误区3:认为模板引用是响应式的
模板引用本身并不是响应式的,不能在模板中直接用它进行数据绑定,比如{{ titleRef.value.textContent }},这种写法是无效的,模板引用仅用于脚本中手动操作DOM。
误区4:子组件未暴露属性,父组件直接访问
父组件通过模板引用访问子组件时,若子组件未通过defineExpose暴露属性和方法,父组件会无法访问,导致报错。必须确保子组件暴露需要被访问的内容。
实战总结:模板引用与生命周期的最佳搭配
模板引用和生命周期钩子是Vue中手动操作DOM的“黄金搭档”,二者的搭配使用,能让我们的代码更安全、更高效。结合前面的知识点,总结一下核心使用场景和最佳实践:
- 获取DOM元素/子组件实例:使用模板引用,普通DOM元素直接注册,子组件需配合
defineExpose暴露内容; - 初始化DOM操作(如聚焦、修改样式):放在
onMounted中,确保DOM已挂载; - 数据更新后调整DOM:放在
onUpdated中,确保DOM已完成更新; - 清理DOM资源:放在
onUnmounted中,避免内存泄漏; - 多个DOM元素收集:使用函数式ref,配合数组存储,方便批量操作。
其实,Vue的设计理念是“尽量减少手动DOM操作”,但这并不意味着完全禁止。模板引用和生命周期钩子的存在,是为了让我们在必要时,能以更规范、更安全的方式操作DOM,而不是直接操作document.getElementById这类原生API——这种方式会脱离Vue的组件体系,导致代码难以维护。
掌握模板引用和生命周期钩子,能让你在面对复杂DOM操作场景时更加从容,无论是集成第三方库,还是实现特殊交互效果,都能游刃有余。后续我们还会讲解与二者互补的“侦听器(watch)”,进一步解锁Vue响应式开发的更多技巧。