一、什么是模板引用?就是你给元素起了个名字
在原生JS里,你想操作一个输入框,得先给它加个id,然后用 document.getElementById('xxx') 去找到它。Vue里有个更优雅的方式:模板引用(Template Refs)。
核心就两步:
-
在模板里的元素上写
ref="你起的名字" -
在
<script setup>里声明一个同名 的ref变量
Vue就会自动把那个元素塞到这个变量里,挂载完成后你就能直接访问了。
下面我们直接用案例说话。
二、案例1:自动聚焦输入框
这是最常用的场景:页面一打开,输入框自动获得焦点。
vue
<template>
<div>
<h3>自动聚焦输入框</h3>
<!--
在 input 元素上写 ref="myInput"
这就相当于告诉Vue:把这个元素存到 myInput 变量里
-->
<input ref="myInput" type="text" placeholder="页面加载完我就自动聚焦" />
</div>
</template>
<script setup>
// 引入 ref 和 onMounted
import { ref, onMounted } from 'vue'
// 声明一个 ref 变量,名字必须和模板里的 ref="myInput" 一致
// 初始值写 null,因为挂载前元素还不存在
const myInput = ref(null)
// 在 onMounted 里操作 DOM,因此时组件已挂载到页面上
onMounted(() => {
// myInput.value 就是那个 input 元素本身
// 直接调用原生 DOM 的 focus() 方法让输入框获得焦点
myInput.value.focus()
})
</script>
代码拆解:
-
<input ref="myInput">:给元素打个标记。 -
const myInput = ref(null):创建一个名为myInput的响应式容器,初始为null。 -
挂载后,Vue 自动把那个 input 元素赋值给
myInput.value。 -
之后你就能通过
myInput.value访问原生 DOM 了,调用它的任何方法、读任何属性都行。
三、案例2:点击按钮让输入框获得焦点
vue
<template>
<div>
<input ref="inputBox" type="text" placeholder="点按钮聚焦我" />
<button @click="focusInput">点击聚焦</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const inputBox = ref(null)
function focusInput() {
// 拿到 input 元素,调用 focus
inputBox.value.focus()
}
</script>
关键点: ref 不仅能在挂载时用,任何时候只要元素存在,你都能通过 .value 拿到它。
四、案例3:读取输入框的值(不用v-model)
虽然大部分时候我们用 v-model 绑定值,但有些场景直接操作 DOM 更灵活,比如和第三方库配合。
vue
<template>
<div>
<input ref="nameInput" type="text" placeholder="输入你的名字" />
<button @click="showName">显示名字</button>
<p v-if="name">你的名字是:{{ name }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const nameInput = ref(null)
const name = ref('')
function showName() {
// 直接读取 input 元素的 value 属性
name.value = nameInput.value.value
}
</script>
解释: nameInput.value 是原生 input 元素,.value(注意这是原生的value属性)就是输入框里的内容。
五、案例4:在 v-for 循环里使用 ref
有时候你需要拿到循环生成的多个元素,比如一个列表,你想让某个项高亮。
vue
<template>
<div>
<ul>
<!--
在 v-for 里写 ref,Vue 会把所有同名的 ref 存成一个数组
注意:ref 名相同,Vue3 会自动收集为数组
-->
<li
v-for="(item, index) in items"
:key="item.id"
:ref="el => setItemRef(el, index)"
:style="{ color: activeIndex === index ? 'red' : 'black' }"
>
{{ item.name }}
</li>
</ul>
<button @click="highlightRandom">随机高亮一项</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橘子' }
])
// 定义一个数组来存放所有 li 元素
const itemRefs = ref([])
const activeIndex = ref(-1)
// 用函数形式的 ref 来收集元素
function setItemRef(el, index) {
if (el) {
itemRefs.value[index] = el
}
}
function highlightRandom() {
// 随机选一个索引
const randomIndex = Math.floor(Math.random() * items.value.length)
activeIndex.value = randomIndex
}
</script>
说明: 在 v-for 里,Vue3 推荐使用函数形式的 ref ,因为直接写 ref="xxx" 会覆盖成最后一个元素。函数写法可以精确控制每个元素存到数组的哪个位置。
六、案例5:在组件上使用 ref
ref 不仅能拿普通 DOM 元素,还能拿子组件实例 。默认情况下,拿到的子组件实例里什么都看不到 ,因为子组件的内部状态默认是封闭的。但你可以用 defineExpose 把东西暴露出来。
6.1 父组件拿子组件实例(什么都没暴露时)
vue
<!-- 父组件 -->
<template>
<div>
<Child ref="childRef" />
<button @click="logChild">打印子组件实例</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
function logChild() {
// 默认情况下,打印出来的 childRef.value 是一个空对象 {}
console.log(childRef.value)
}
</script>
vue
<!-- 子组件 Child.vue -->
<template>
<p>我是子组件</p>
</template>
<script setup>
// 啥也不暴露,父组件拿到的就是空对象
</script>
七、defineExpose:把子组件的东西亮出来
defineExpose 的作用就是让子组件"授权"某些数据或方法可以被父组件访问。
7.1 暴露一个方法
vue
<!-- 子组件 CountControl.vue -->
<template>
<div>
<p>计数:{{ count }}</p>
<!-- 内部按钮供自己用 -->
<button @click="add">内部加1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
function reset() {
count.value = 0
}
// 把 reset 方法暴露出去,让父组件能调用
defineExpose({
reset
// 也可以暴露 count,但不推荐直接暴露数据让外部改,通常暴露方法
})
</script>
vue
<!-- 父组件 -->
<template>
<div>
<CountControl ref="controlRef" />
<button @click="resetFromParent">父组件里的重置按钮</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import CountControl from './CountControl.vue'
const controlRef = ref(null)
function resetFromParent() {
// 调用子组件暴露的 reset 方法
controlRef.value.reset()
}
</script>
八、实战案例1:封装一个受父组件控制的模态框
模态框(弹窗)是一个非常经典的组件。我们希望父组件能够随时调用子组件的 open() 和 close() 方法来控制显示隐藏。
子组件 Modal.vue
vue
<template>
<!-- 用 v-if 控制整个弹窗的显示隐藏 -->
<div v-if="visible" class="modal-mask" @click.self="close">
<div class="modal-box">
<div class="modal-header">
<!-- 默认显示标题,也可以用插槽 -->
<slot name="header">{{ title }}</slot>
</div>
<div class="modal-body">
<!-- 默认插槽,放弹窗主体内容 -->
<slot>弹窗内容</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 接收一个 title prop,但不强制,父组件也可以用插槽自定义头部
defineProps({
title: {
type: String,
default: '提示'
}
})
// 内部控制显示隐藏的变量
const visible = ref(false)
// 打开弹窗的方法
function open() {
visible.value = true
}
// 关闭弹窗的方法
function close() {
visible.value = false
}
// 把 open 和 close 暴露给父组件
defineExpose({
open,
close
})
</script>
<style scoped>
.modal-mask {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-box {
background: white;
border-radius: 8px;
min-width: 300px;
max-width: 90%;
}
.modal-header, .modal-body, .modal-footer {
padding: 15px;
}
.modal-header {
border-bottom: 1px solid #eee;
font-weight: bold;
}
.modal-footer {
border-top: 1px solid #eee;
text-align: right;
}
.modal-footer button {
padding: 6px 15px;
cursor: pointer;
}
</style>
父组件使用
vue
<template>
<div>
<button @click="showModal">打开弹窗</button>
<!-- 给 Modal 加 ref,以便调用它的方法 -->
<Modal ref="modalRef" title="用户协议">
<!-- 默认插槽:弹窗内容 -->
<p>欢迎注册,请阅读以下协议...</p>
<p>1. 遵守法律法规</p>
<p>2. 保护个人隐私</p>
<!-- 具名插槽 footer:自定义底部按钮 -->
<template #footer>
<button @click="agree">同意</button>
<button @click="modalRef.close()">取消</button>
</template>
</Modal>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
// 拿到子组件实例
const modalRef = ref(null)
function showModal() {
// 调用子组件的 open 方法
modalRef.value.open()
}
function agree() {
alert('感谢同意!')
// 调用子组件的 close 方法关闭弹窗
modalRef.value.close()
}
</script>
这个例子展示了最典型的用法: 父组件通过 ref 拿到子组件,然后调用子组件暴露出来的方法,完全控制子组件的行为。
九、实战案例2:封装一个轮播图组件,父组件可控制上一张/下一张
再来看一个稍微复杂点的组件:轮播图。我们希望父组件能调用 prev() 和 next() 方法,并且还能知道当前是第几张。
子组件 Swiper.vue
vue
<template>
<div class="swiper">
<div class="slides">
<!-- 根据 currentIndex 显示对应的图片 -->
<img
v-for="(img, index) in images"
:key="index"
:src="img"
:class="{ active: index === currentIndex }"
:alt="'图片' + (index + 1)"
/>
</div>
<!-- 小圆点指示器 -->
<div class="dots">
<span
v-for="(img, index) in images"
:key="index"
:class="{ active: index === currentIndex }"
@click="goTo(index)"
></span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 接收图片数组
const props = defineProps({
images: {
type: Array,
default: () => []
}
})
// 当前显示的第几张,从 0 开始
const currentIndex = ref(0)
// 下一张
function next() {
// 如果已经是最后一张,就回到第一张(循环)
if (currentIndex.value < props.images.length - 1) {
currentIndex.value++
} else {
currentIndex.value = 0
}
}
// 上一张
function prev() {
if (currentIndex.value > 0) {
currentIndex.value--
} else {
// 如果已经是第一张,就跳到最后一张
currentIndex.value = props.images.length - 1
}
}
// 跳转到指定张
function goTo(index) {
currentIndex.value = index
}
// 暴露方法给父组件
defineExpose({
next,
prev,
// 也可以暴露当前索引,不过推荐只暴露方法
currentIndex
})
</script>
<style scoped>
.swiper {
position: relative;
width: 400px;
height: 250px;
overflow: hidden;
}
.slides img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.5s;
}
.slides img.active {
opacity: 1;
}
.dots {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.dots span {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255,255,255,0.6);
cursor: pointer;
}
.dots span.active {
background: white;
}
</style>
父组件使用
vue
<template>
<div>
<Swiper ref="swiperRef" :images="picList" />
<div style="margin-top: 10px;">
<button @click="swiperRef.prev()">上一张</button>
<button @click="swiperRef.next()">下一张</button>
</div>
<p>当前是第 {{ swiperRef?.currentIndex + 1 }} 张</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Swiper from './Swiper.vue'
const swiperRef = ref(null)
// 随便找几张示例图片
const picList = ref([
'https://picsum.photos/id/1/400/250',
'https://picsum.photos/id/2/400/250',
'https://picsum.photos/id/3/400/250'
])
</script>
注意: 父组件通过 swiperRef.value.currentIndex 直接读取了子组件暴露的 currentIndex。虽然可行,但通常建议只暴露方法,让子组件内部维护状态,这样耦合度更低。这里为了方便演示就这么写了。
十、总结
今天我们学习了两个东西:
-
模板引用
ref:给元素或组件打个标记,在JS里用同名的ref变量拿到它,然后就能操作原生DOM或调用组件方法。 -
defineExpose:子组件可以决定让父组件看到自己哪些方法和数据,就像遥控器上的按钮。
常见使用场景:
-
自动聚焦输入框
-
调用子组件的打开/关闭、上一页/下一页等控制方法
-
和第三方库配合,拿到DOM元素进行初始化(比如图表库)
这两个技能配合起来,你的组件就能从"纯展示"变成"可操控",写起复杂交互来会顺手得多。
下篇咱们接着聊Vue里的另一个常用技能:自定义指令,让页面交互更灵活。有问题评论区说,我挨个回!