url: /posts/ddbce4f2a23aa72c96b1c0473900321e/
title: 快速入门Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗?
date: 2025-11-03T02:55:45+08:00
lastmod: 2025-11-03T02:55:45+08:00
author: cmdragon
summary:
Vue中的模板引用(Template Refs)用于在声明式编程中直接操作DOM或访问子组件实例。通过
ref属性标记元素或组件,并在
setup中使用同名响应式变量访问。子组件需通过
defineExpose暴露内部方法或属性供父组件调用。操作DOM时,应在
onMounted或
nextTick中确保DOM已渲染。常见应用包括自动聚焦输入框、集成第三方库和动态获取元素尺寸。
categories:
vuetags:
基础入门 Vue 模板引用DOM操作组件通信nextTickdefineExpose最佳实践
扫描二维码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
发现1000+提升效率与开发的AI工具和实用程序:https://tools.cmdragon.cn/
在Vue的声明式编程模型中,我们通常不需要直接操作DOM——Vue会根据数据自动更新视图。但有些场景必须直接接触DOM:比如聚焦输入框、获取元素尺寸、集成第三方DOM库(如Chart.js)。这时候,**模板引用(Template Refs)**就成了连接声明式世界与命令式DOM操作的桥梁。
模板引用是Vue提供的一种标记DOM元素或组件的方式:通过给元素或组件添加
ref属性(类似“标签”),我们可以在
setup中通过同名的响应式变量,直接访问对应的DOM元素或组件实例。
使用模板引用的步骤非常简单,只需两步:
在模板中标记元素:给需要引用的元素添加
ref="xxx"属性;在
setup中创建响应式变量:用
ref(null)创建同名变量,Vue会自动将DOM元素赋值给它。
注意:模板引用的变量必须用
ref(null)初始化(初始值为
null),因为Vue会在组件挂载后才将DOM元素赋值给它。
下面是一个最常见的场景——页面加载后自动聚焦输入框:
<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()方法。
模板引用不仅能标记DOM元素,还能标记子组件。但组件的引用有个特殊规则:默认情况下,子组件的内部状态和方法是“私有的”,父组件无法直接访问。如果要让父组件调用子组件的方法或访问其内部元素,必须用
defineExpose显式暴露。
当你给子组件添加
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>
defineExpose如果父组件需要访问子组件的内部方法或非根元素,子组件必须用
defineExpose将这些内容“公开”。
defineExpose是Vue 3的内置API,专门用于暴露
setup中的内容给父组件。
假设子组件有一个“点击按钮”的方法,父组件需要直接调用它:
子组件(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虽然灵活,但容易破坏Vue的响应式流程。以下是避免踩坑的关键原则:
DOM元素只有在组件挂载后才会存在,因此:
不要在
setup的顶级 scope 直接访问模板引用(此时
xxx.value还是
null);不要在
onBeforeMount钩子中操作DOM(组件还没挂载,元素未渲染);安全时机:
onMounted钩子(组件首次挂载完成)、
nextTick(DOM更新后)。
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状态。
假设我们有一个弹窗,需要根据按钮位置动态调整坐标:
<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状态。
模板引用的底层逻辑其实很简单,我们可以用“快递比喻”理解:
贴标签:你给DOM元素贴了个
ref="inputRef"的“快递单”;派件:Vue在组件挂载时(
onMounted前),会把DOM元素“快递”到
setup中同名的
inputRef变量里;签收:
onMounted钩子触发时,你已经“签收”了快递(
inputRef.value指向DOM元素)。
对于组件引用,Vue会先将子组件的根元素赋值给父组件的ref变量;如果子组件用
defineExpose暴露了内容,Vue会将暴露的对象与根元素合并,让父组件能访问内部方法。
模板引用的核心价值是解决“声明式无法覆盖的场景”,以下是几个典型案例:
比如登录页加载后,自动聚焦用户名输入框(见1.3的示例)。
很多第三方库(如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>
比如响应式布局中,需要根据容器宽度调整内部元素的排版:
<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>
答案:
子组件用
defineExpose暴露内部方法(如
defineExpose({ handleClick }));父组件用
ref引用子组件(
const childRef = ref(null));通过
childRef.value.xxx调用暴露的方法(如
childRef.value.handleClick())。
答案:
原因:Vue的DOM更新是异步的,修改数据后DOM不会立即更新;解决:用
nextTick等待DOM更新完成(如
await nextTick()后再获取尺寸)。
Cannot read properties of null (reading 'focus')错误原因:
访问模板引用时,对应的DOM元素还未渲染(如
setup顶级scope、
onBeforeMount);元素被
v-if条件渲染隐藏(如
v-if="show"刚设为
true,DOM还未更新)。
解决方法:
将操作放在
onMounted钩子中;用
nextTick等待DOM更新(如条件渲染的场景);检查
xxx.value是否为
null(防御性编程):
if (inputRef.value) {
inputRef.value.focus()
}
Component is missing expose declaration错误原因:
父组件引用了子组件的内部方法,但子组件未用
defineExpose暴露。
解决方法:
在子组件中用
defineExpose暴露需要的方法或属性:
// 子组件中
defineExpose({
handleClick, // 暴露方法
count // 暴露属性
})
参考链接:https://vuejs.org/guide/essentials/template-refs.html
余下文章内容请点击跳转至 个人博客页面 或者 扫描二维码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长,阅读完整的文章:Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗?