快速入门Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗?

90 阅读16分钟

一、模板引用的基本概念与用法

在Vue的声明式编程模型中,我们通常不需要直接操作DOM——Vue会根据数据自动更新视图。但有些场景必须直接接触DOM:比如聚焦输入框、获取元素尺寸、集成第三方DOM库(如Chart.js)。这时候,**模板引用(Template Refs)**就成了连接声明式世界与命令式DOM操作的桥梁。

1.1 什么是模板引用?

模板引用是Vue提供的一种标记DOM元素或组件的方式:通过给元素或组件添加ref属性(类似“标签”),我们可以在setup中通过同名的响应式变量,直接访问对应的DOM元素或组件实例。

1.2 如何声明和使用模板引用?

使用模板引用的步骤非常简单,只需两步:

  1. 在模板中标记元素:给需要引用的元素添加ref="xxx"属性;
  2. setup中创建响应式变量:用ref(null)创建同名变量,Vue会自动将DOM元素赋值给它。

注意:模板引用的变量必须用ref(null)初始化(初始值为null),因为Vue会在组件挂载后才将DOM元素赋值给它。

1.3 示例:自动聚焦输入框

下面是一个最常见的场景——页面加载后自动聚焦输入框:

<template>
  <!-- 用ref标记输入框 -->
  <input ref="inputRef" type="text" placeholder="请输入内容" />
</template>

<script setup>
// 1. 导入需要的API:ref(创建响应式变量)、onMounted(生命周期钩子)
import { ref, onMounted } from 'vue'

// 2. 创建响应式变量,初始值为null(此时DOM还未渲染)
const inputRef = ref(null)

// 3. 组件挂载后(DOM已渲染),聚焦输入框
onMounted(() => {
  // inputRef.value 此时指向模板中的<input>元素
  inputRef.value.focus() 
})
</script>

代码解释

  • ref="inputRef":给输入框贴了个“标签”,告诉Vue“我要引用这个元素”;
  • const inputRef = ref(null):在setup中创建一个“容器”,等待Vue把DOM元素装进来;
  • onMounted:组件挂载完成的生命周期钩子,此时DOM已经渲染完成,inputRef.value不再是null,可以安全调用focus()方法。

余下文章内容请点击跳转至 个人博客页面 或者 扫描二维码关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗?

往期文章归档
免费好用的热门在线工具

二、组件的模板引用与暴露

模板引用不仅能标记DOM元素,还能标记子组件。但组件的引用有个特殊规则:默认情况下,子组件的内部状态和方法是“私有的”,父组件无法直接访问。如果要让父组件调用子组件的方法或访问其内部元素,必须用defineExpose显式暴露。

2.1 引用子组件的默认行为

当你给子组件添加ref属性时,父组件拿到的是子组件的根元素(如果子组件有多个根元素,会报错)。比如:

<!-- ParentComponent.vue -->
<template>
  <!-- 引用子组件 -->
  <ChildComponent ref="childRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

const childRef = ref(null)

onMounted(() => {
  // childRef.value 指向子组件的根元素(比如<div>)
  console.log(childRef.value) 
})
</script>

2.2 暴露子组件内部内容:defineExpose

如果父组件需要访问子组件的内部方法或非根元素,子组件必须用defineExpose将这些内容“公开”。defineExpose是Vue 3的内置API,专门用于暴露setup中的内容给父组件。

2.3 示例:父组件调用子组件的方法

假设子组件有一个“点击按钮”的方法,父组件需要直接调用它:

子组件(ChildComponent.vue)

<template>
  <button @click="handleClick">子组件按钮</button>
</template>

<script setup>
// 导入defineExpose API
import { defineExpose } from 'vue'

// 子组件的内部方法
const handleClick = () => {
  console.log('子组件按钮被点击!')
}

// 关键:将handleClick方法暴露给父组件
defineExpose({
  handleClick
})
</script>

父组件(ParentComponent.vue)

<template>
  <ChildComponent ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 引用子组件实例
const childRef = ref(null)

// 父组件调用子组件方法
const callChildMethod = () => {
  // 通过childRef.value访问子组件暴露的handleClick
  childRef.value.handleClick() 
}
</script>

效果:点击父组件的“调用子组件方法”按钮,会触发子组件的handleClick,控制台输出“子组件按钮被点击!”。

三、DOM操作的最佳实践

直接操作DOM虽然灵活,但容易破坏Vue的响应式流程。以下是避免踩坑的关键原则

3.1 何时可以安全操作DOM?

DOM元素只有在组件挂载后才会存在,因此:

  • 不要在setup的顶级 scope 直接访问模板引用(此时xxx.value还是null);
  • 不要在onBeforeMount钩子中操作DOM(组件还没挂载,元素未渲染);
  • 安全时机onMounted钩子(组件首次挂载完成)、nextTick(DOM更新后)。

3.2 nextTick:处理DOM更新后的操作

Vue的DOM更新是异步的——当你修改数据后,Vue不会立即更新DOM,而是等到下一个“事件循环”再批量更新。如果此时直接访问DOM,拿到的会是旧的DOM状态

比如,修改message后想立即获取元素尺寸:

<template>
  <div ref="boxRef">{{ message }}</div>
  <button @click="updateMessage">更新内容</button>
</template>

<script setup>
import { ref, nextTick } from 'vue'

const message = ref('初始内容')
const boxRef = ref(null)

const updateMessage = async () => {
  message.value = '新的内容' // 修改数据
  
  // 错误:此时DOM还未更新,拿到的是旧尺寸
  console.log('旧尺寸:', boxRef.value.offsetWidth) 
  
  // 正确:用nextTick等待DOM更新完成
  await nextTick()
  console.log('新尺寸:', boxRef.value.offsetWidth) 
}
</script>

nextTick的作用:将回调函数延迟到下一次DOM更新循环后执行,确保能拿到最新的DOM状态。

3.3 示例:动态调整弹窗位置

假设我们有一个弹窗,需要根据按钮位置动态调整坐标:

<template>
  <button ref="btnRef" @click="showPopup">打开弹窗</button>
  <div ref="popupRef" class="popup" v-if="isPopupShow">
    我是弹窗
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue'

const btnRef = ref(null)
const popupRef = ref(null)
const isPopupShow = ref(false)

const showPopup = async () => {
  isPopupShow.value = true // 显示弹窗
  
  // 等待弹窗渲染完成
  await nextTick()
  
  // 获取按钮的位置信息
  const btnRect = btnRef.value.getBoundingClientRect()
  
  // 调整弹窗位置(在按钮下方)
  popupRef.value.style.left = `${btnRect.left}px`
  popupRef.value.style.top = `${btnRect.bottom + 10}px`
}
</script>

<style scoped>
.popup {
  position: fixed;
  padding: 10px;
  background: white;
  border: 1px solid #ccc;
}
</style>

效果:点击按钮后,弹窗会精准出现在按钮下方——nextTick确保我们拿到了弹窗和按钮的最新DOM状态。

四、模板引用的原理剖析

模板引用的底层逻辑其实很简单,我们可以用“快递比喻”理解:

  1. 贴标签:你给DOM元素贴了个ref="inputRef"的“快递单”;
  2. 派件:Vue在组件挂载时(onMounted前),会把DOM元素“快递”到setup中同名的inputRef变量里;
  3. 签收onMounted钩子触发时,你已经“签收”了快递(inputRef.value指向DOM元素)。

对于组件引用,Vue会先将子组件的根元素赋值给父组件的ref变量;如果子组件用defineExpose暴露了内容,Vue会将暴露的对象与根元素合并,让父组件能访问内部方法。

五、常见应用场景

模板引用的核心价值是解决“声明式无法覆盖的场景”,以下是几个典型案例:

5.1 表单元素的自动聚焦

比如登录页加载后,自动聚焦用户名输入框(见1.3的示例)。

5.2 第三方DOM库的集成

很多第三方库(如Chart.js、Swiper)需要直接操作DOM元素。以Chart.js为例:

<template>
  <canvas ref="chartRef"></canvas>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import Chart from 'chart.js/auto'

const chartRef = ref(null)
let chartInstance = null

onMounted(() => {
  // 用模板引用获取canvas元素,初始化Chart实例
  chartInstance = new Chart(chartRef.value, {
    type: 'bar',
    data: {
      labels: ['周一', '周二', '周三'],
      datasets: [{
        label: '销售额',
        data: [1000, 1500, 1200],
        backgroundColor: '#42b983'
      }]
    }
  })
})
</script>

5.3 动态获取元素尺寸

比如响应式布局中,需要根据容器宽度调整内部元素的排版:

<template>
  <div ref="containerRef" class="container">
    <div class="item" v-for="item in items" :key="item.id">{{ item.name }}</div>
  </div>
</template>

<script setup>
import { ref, onMounted, onResize } from 'vue'

const containerRef = ref(null)
const items = ref([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }])

// 监听窗口resize事件
onResize(() => {
  if (containerRef.value) {
    const width = containerRef.value.offsetWidth
    console.log('容器宽度:', width)
    // 根据宽度调整item的排列方式(如flex-wrap)
  }
})

onMounted(() => {
  // 初始加载时获取尺寸
  onResize()
})
</script>

六、课后Quiz

6.1 问题1:父组件如何调用子组件内部的方法?

答案

  1. 子组件用defineExpose暴露内部方法(如defineExpose({ handleClick }));
  2. 父组件用ref引用子组件(const childRef = ref(null));
  3. 通过childRef.value.xxx调用暴露的方法(如childRef.value.handleClick())。

6.2 问题2:为什么修改数据后直接获取DOM尺寸会得到旧值?如何解决?

答案

  • 原因:Vue的DOM更新是异步的,修改数据后DOM不会立即更新;
  • 解决:用nextTick等待DOM更新完成(如await nextTick()后再获取尺寸)。

七、常见报错与解决方案

7.1 报错:Cannot read properties of null (reading 'focus')

错误原因

  • 访问模板引用时,对应的DOM元素还未渲染(如setup顶级scope、onBeforeMount);
  • 元素被v-if条件渲染隐藏(如v-if="show"刚设为true,DOM还未更新)。

解决方法

  1. 将操作放在onMounted钩子中;
  2. nextTick等待DOM更新(如条件渲染的场景);
  3. 检查xxx.value是否为null(防御性编程):
    if (inputRef.value) {
      inputRef.value.focus()
    }
    

7.2 报错:Component is missing expose declaration

错误原因
父组件引用了子组件的内部方法,但子组件未用defineExpose暴露。

解决方法
在子组件中用defineExpose暴露需要的方法或属性:

// 子组件中
defineExpose({
  handleClick, // 暴露方法
  count        // 暴露属性
})

参考链接:vuejs.org/guide/essen…