Vue模板引用和组件暴露:ref拿DOM、defineExpose调方法,案例多到眼花

一、什么是模板引用?就是你给元素起了个名字

在原生JS里,你想操作一个输入框,得先给它加个id,然后用 document.getElementById('xxx') 去找到它。Vue里有个更优雅的方式:模板引用(Template Refs)

核心就两步:

  1. 在模板里的元素上写 ref="你起的名字"

  2. <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里的另一个常用技能:自定义指令,让页面交互更灵活。有问题评论区说,我挨个回!

相关推荐
杨超越luckly1 小时前
Agent应用指南:利用GET请求获取理想汽车门店位置信息
前端·python·html·汽车·可视化
薛定谔的猫-菜鸟程序员1 小时前
从Electron到Tauri,Rust+Vue(Tauri) 实现超高性能桌面日志应用开发,以及开发避坑指南
vue.js·rust·electron
小雨下雨的雨7 小时前
井字棋AI机器人实现详解 - Minimax算法实战-鸿蒙PC Electron框架完成
前端·人工智能·算法·华为·electron·鸿蒙
ZC跨境爬虫11 小时前
跟着 MDN 学JavaScript day_7:数学运算与逻辑判断实战测试
开发语言·前端·javascript·学习·ecmascript
fangdengfu12311 小时前
ES分析系统各个服务日志占用量
java·前端·elasticsearch
凌云拓界11 小时前
文件管理:让AI安全操作你的电脑 ——CogitoAgent开发实战(三)
javascript·人工智能·架构·开源·node.js
凌云拓界11 小时前
联网能力:让AI看见更广阔的世界 ——CogitoAgent开发实战(四)
javascript·人工智能·架构·node.js·创业创新
JustHappy12 小时前
古法编程秘籍(六):程序到底是怎么跑起来的?从 IO 到中断,一次讲明白
前端·后端·全栈