
一、什么是模板引用就是你给元素起了个名字在原生JS里你想操作一个输入框得先给它加个id然后用document.getElementById(xxx)去找到它。Vue里有个更优雅的方式模板引用Template Refs。核心就两步在模板里的元素上写ref你起的名字在script setup里声明一个同名的ref变量Vue就会自动把那个元素塞到这个变量里挂载完成后你就能直接访问了。下面我们直接用案例说话。二、案例1自动聚焦输入框这是最常用的场景页面一打开输入框自动获得焦点。vuetemplate div h3自动聚焦输入框/h3 !-- 在 input 元素上写 refmyInput 这就相当于告诉Vue把这个元素存到 myInput 变量里 -- input refmyInput typetext placeholder页面加载完我就自动聚焦 / /div /template script setup // 引入 ref 和 onMounted import { ref, onMounted } from vue // 声明一个 ref 变量名字必须和模板里的 refmyInput 一致 // 初始值写 null因为挂载前元素还不存在 const myInput ref(null) // 在 onMounted 里操作 DOM因此时组件已挂载到页面上 onMounted(() { // myInput.value 就是那个 input 元素本身 // 直接调用原生 DOM 的 focus() 方法让输入框获得焦点 myInput.value.focus() }) /script代码拆解input refmyInput给元素打个标记。const myInput ref(null)创建一个名为myInput的响应式容器初始为null。挂载后Vue 自动把那个 input 元素赋值给myInput.value。之后你就能通过myInput.value访问原生 DOM 了调用它的任何方法、读任何属性都行。三、案例2点击按钮让输入框获得焦点vuetemplate div input refinputBox typetext placeholder点按钮聚焦我 / button clickfocusInput点击聚焦/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 更灵活比如和第三方库配合。vuetemplate div input refnameInput typetext placeholder输入你的名字 / button clickshowName显示名字/button p v-ifname你的名字是{{ 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有时候你需要拿到循环生成的多个元素比如一个列表你想让某个项高亮。vuetemplate div ul !-- 在 v-for 里写 refVue 会把所有同名的 ref 存成一个数组 注意ref 名相同Vue3 会自动收集为数组 -- li v-for(item, index) in items :keyitem.id :refel setItemRef(el, index) :style{ color: activeIndex index ? red : black } {{ item.name }} /li /ul button clickhighlightRandom随机高亮一项/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因为直接写refxxx会覆盖成最后一个元素。函数写法可以精确控制每个元素存到数组的哪个位置。六、案例5在组件上使用 refref 不仅能拿普通 DOM 元素还能拿子组件实例。默认情况下拿到的子组件实例里什么都看不到因为子组件的内部状态默认是封闭的。但你可以用defineExpose把东西暴露出来。6.1 父组件拿子组件实例什么都没暴露时vue!-- 父组件 -- template div Child refchildRef / button clicklogChild打印子组件实例/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) } /scriptvue!-- 子组件 Child.vue -- template p我是子组件/p /template script setup // 啥也不暴露父组件拿到的就是空对象 /script七、defineExpose把子组件的东西亮出来defineExpose的作用就是让子组件“授权”某些数据或方法可以被父组件访问。7.1 暴露一个方法vue!-- 子组件 CountControl.vue -- template div p计数{{ count }}/p !-- 内部按钮供自己用 -- button clickadd内部加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但不推荐直接暴露数据让外部改通常暴露方法 }) /scriptvue!-- 父组件 -- template div CountControl refcontrolRef / button clickresetFromParent父组件里的重置按钮/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.vuevuetemplate !-- 用 v-if 控制整个弹窗的显示隐藏 -- div v-ifvisible classmodal-mask click.selfclose div classmodal-box div classmodal-header !-- 默认显示标题也可以用插槽 -- slot nameheader{{ title }}/slot /div div classmodal-body !-- 默认插槽放弹窗主体内容 -- slot弹窗内容/slot /div div classmodal-footer slot namefooter button clickclose关闭/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父组件使用vuetemplate div button clickshowModal打开弹窗/button !-- 给 Modal 加 ref以便调用它的方法 -- Modal refmodalRef title用户协议 !-- 默认插槽弹窗内容 -- p欢迎注册请阅读以下协议.../p p1. 遵守法律法规/p p2. 保护个人隐私/p !-- 具名插槽 footer自定义底部按钮 -- template #footer button clickagree同意/button button clickmodalRef.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.vuevuetemplate div classswiper div classslides !-- 根据 currentIndex 显示对应的图片 -- img v-for(img, index) in images :keyindex :srcimg :class{ active: index currentIndex } :alt图片 (index 1) / /div !-- 小圆点指示器 -- div classdots span v-for(img, index) in images :keyindex :class{ active: index currentIndex } clickgoTo(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父组件使用vuetemplate div Swiper refswiperRef :imagespicList / div stylemargin-top: 10px; button clickswiperRef.prev()上一张/button button clickswiperRef.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里的另一个常用技能自定义指令让页面交互更灵活。有问题评论区说我挨个回