Vue模板引用与生命周期钩子:手动操作DOM的正确打开方式

0 阅读9分钟

在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>

image.png

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未挂载、已销毁等情况导致的报错。其中,最常用的三个钩子分别是onMountedonUpdatedonUnmounted

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>

image.png

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>

image.png

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:异步注册生命周期钩子

生命周期钩子必须同步注册,不能在setTimeoutPromise.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响应式开发的更多技巧。