1.先搞懂,useTemplateRefs 到底是什么?
在Vue3开发里,不少同学遇到过这种麻烦事:列表循环渲染时,每个项都需要操作DOM或者组件实例,得一个个给ref,再手动存到数组里;表单里多个输入框要统一验证,ref变量声明了一大串……要是能“一键收集”所有同类型的ref,代码能少写一半!Vue3.3+推出的useTemplateRefs就是干这个的,但很多人对它的用法、场景还是雾里看花,今天用问答形式,把useTemplateRefs从基础到实战掰碎了讲,帮你在项目里高效用起来~
简单说,useTemplateRefs是Vue3.3+新增的**组合式API**,专门用来批量收集模板里“同名ref”的实例,比如你在v-for循环里给每个元素都绑了`ref="itemRef"`,用useTemplateRefs就能一次性把这些ref全捞出来,不用手动push到数组里。
它的用法很直观:在`
对比普通模板ref(单个声明`const refName = ref()`,模板绑`ref="refName"`),useTemplateRefs解决的是**“批量收集同名ref”**的痛点——不用再为每个循环项手动维护数组,Vue帮你自动收集。
和普通模板ref比,useTemplateRefs 优势在哪?
普通模板ref处理“多个同类型元素”时,代码又繁琐又容易出错,举个🌰:做一个hover时显示操作按钮的列表,每个项要绑ref来控制按钮显示。
普通ref写法(痛点明显):
<script setup> import { ref, onMounted } from 'vue' const itemRefs = ref([]) // 手动声明数组存ref onMounted(() => { // 这里得确保itemRefs.value长度和列表一致,否则可能漏或多 console.log(itemRefs.value) }) </script> <template> <div v-for="(item, idx) in list" :key="idx" :ref="(el) => itemRefs.value[idx] = el" > {{ item }} </div> </template>
这里得手动写回调把每个el塞到数组对应位置,一旦列表增删,还得操心数组长度是否同步,很容易漏判;而且代码里“手动维护数组”的逻辑很冗余。
useTemplateRefs写法(清爽太多):
<script setup> import { useTemplateRefs, onMounted } from 'vue' const refs = useTemplateRefs() onMounted(() => { const allItems = refs().itemRef // 直接拿到所有项的ref数组 allItems.forEach(el => { // 批量给每个项加hover事件(示例逻辑) el.addEventListener('mouseenter', () => { ... }) }) }) </script> <template> <div v-for="item in list" :key="item.id" ref="itemRef"> {{ item.name }} </div> </template>
能看出区别吧?useTemplateRefs帮你自动收集所有`ref="itemRef"`的元素,不用手动处理数组索引、长度,代码量少了一半,维护性直接拉满,特别是列表很长、ref操作逻辑复杂时,这种优势更明显。
实际开发中,哪些场景非用它不可?
不是所有场景都需要useTemplateRefs,但碰到这几类需求,用它能省大功夫:
场景1:列表项的交互/动画管理
todo 列表,每个项hover显示删除按钮、点击展开详情、滑动时触发动画……这些都需要操作每个项的DOM,用useTemplateRefs收集所有项的ref后,能批量绑定事件、修改样式。
再比如“无限滚动列表”,需要给每个新渲染的项加懒加载指令(比如图片懒加载),用useTemplateRefs收集后,一次性给所有项的img绑自定义指令,比逐个处理高效多了。
场景2:表单的批量验证/操作
做登录注册表单时,多个输入框(用户名、密码、验证码)需要在提交前统一验证是否为空、格式是否正确,用useTemplateRefs把所有输入框的ref收集到一个对象里,提交时循环验证,不用每个输入框声明单独的ref变量。
```vue ```场景3:动态组件的实例管理
用`v-for`渲染多个动态组件(`
场景4:自定义指令的批量绑定
如果有自定义指令(v-permission`控制权限显示),需要给多个元素批量绑指令,useTemplateRefs收集ref后,能一次性遍历所有元素,动态添加/移除指令逻辑,比在模板里重复写指令更灵活。
代码怎么写?一步步教你用
useTemplateRefs的核心是“**声明收集函数 → 模板绑定同名ref → 调用函数获取所有实例**”,下面分步骤拆解:
步骤1:导入并声明收集函数
在<script setup>
里导入useTemplateRefs,然后声明变量接收返回值(这个变量是函数):
<script setup> import { useTemplateRefs, onMounted } from 'vue' const refs = useTemplateRefs() // 关键:声明收集函数 </script>
步骤2:模板中绑定同名ref
在需要收集的元素上,统一用ref="自定义名称"
,比如v-for循环里的项:
<template> <div v-for="(item, idx) in list" :key="idx" ref="cardRef" <!-- 所有项都绑同一个ref名 --> > {{ item.title }} </div> </template>
步骤3:调用函数,获取所有ref实例
useTemplateRefs返回的函数(比如上面的refs),调用后会返回一个对象,键是ref名称,值是对应所有元素的ref数组,注意:要在DOM渲染完成后调用(比如onMounted、nextTick里),否则可能拿不到最新实例。
<script setup> import { useTemplateRefs, onMounted, nextTick } from 'vue' const refs = useTemplateRefs() const list = ref([/* 数据 */]) onMounted(async () => { await nextTick() // 确保DOM更新完毕 const allCards = refs().cardRef // 拿到所有绑了cardRef的div实例 allCards.forEach(card => { card.style.backgroundColor = '#f5f5f5' // 批量改样式 }) }) </script>
步骤4:后续操作(事件绑定、逻辑处理)
拿到ref数组后,想干啥就干啥:批量加事件、改样式、调用组件方法……比如给每个卡片加点击事件:
allCards.forEach((card, index) => { card.addEventListener('click', () => { console.log(`第${index+1}个卡片被点击了`) }) })
这些“坑”踩过才知道!使用时要避开什么?
useTemplateRefs好用,但不注意细节容易踩坑,提前避坑能少熬夜debug:
坑1:ref命名冲突,不同列表混在一起
如果页面上有两个v-for列表,都用了`ref="itemRef"`,那refs().itemRef会把两个列表的项全装进去,导致逻辑混乱。
**解决:** 给不同列表的ref取独特名称,ref="listAItem"`和`ref="listBItem"`,这样调用时`refs().listAItem`和`refs().listBItem`就互不干扰。坑2:调用时机不对,拿不到最新ref
如果在数据更新后立刻调用refs(),但DOM还没重新渲染,拿到的还是旧数据的ref,比如列表新增了一项,直接调用refs(),新项的ref还没被收集。
**解决:** 用`nextTick`确保DOM更新后再调用。 ```vue const addItem = () => { list.value.push(newItem) nextTick(() => { const newRefs = refs().itemRef // 此时newRefs包含新增项的ref }) } ```坑3:TS类型推导不友好(TypeScript项目)
默认情况下,refs()返回的对象类型是`Record
拿真实案例对比,看它如何简化开发
光说不练假把式,用“列表项hover显示操作按钮”这个常见需求,对比普通ref和useTemplateRefs的写法,感受代码差异。
需求:todo列表,每个项hover时显示“删除”按钮,离开时隐藏。
普通ref写法(代码冗余):
<template> <div v-for="(todo, idx) in todos" :key="idx" :ref="(el) => todoRefs[idx] = el" @mouseenter="showBtn(idx)" @mouseleave="hideBtn(idx)" > {{ todo.name }} <button v-show="btnVisible[idx]">删除</button> </div> </template> <script setup> import { ref } from 'vue' const todos = ref([/* 数据 */]) const todoRefs = ref([]) // 存每个项的ref const btnVisible = ref([]) // 存每个按钮的显示状态 const showBtn = (idx) => { btnVisible.value[idx] = true // 假设还要操作DOM,比如改项的背景色 todoRefs.value[idx].style.backgroundColor = '#eee' } const hideBtn = (idx) => { btnVisible.value[idx] = false todoRefs.value[idx].style.backgroundColor = '' } </script>
这里要维护`todoRefs`数组和`btnVisible`数组,还要在ref回调里手动赋值,逻辑分散,列表长度变化时容易出bug。
useTemplateRefs写法(简洁高效):
<template> <div v-for="todo in todos" :key="todo.id" ref="todoItem" @mouseenter="handleEnter" @mouseleave="handleLeave" > {{ todo.name }} <button v-show="todo.showBtn">删除</button> </div> </template> <script setup> import { useTemplateRefs, ref, nextTick } from 'vue' const todos = ref([/* 数据 */]) const refs = useTemplateRefs() // 批量处理hover逻辑 const handleEnter = (e) => { // 先确保refs已收集最新DOM nextTick(() => { const allItems = refs().todoItem allItems.forEach((item, idx) => { if (item === e.target) { // 找到当前hover的项 todos.value[idx].showBtn = true item.style.backgroundColor = '#eee' } }) }) } const handleLeave = (e) => { nextTick(() => { const allItems = refs().todoItem allItems.forEach((item, idx) => { if (item === e.target) { todos.value[idx].showBtn = false item.style.backgroundColor = '' } }) }) } </script>
能看到:useTemplateRefs把“维护ref数组”的逻辑全交给Vue,代码里不用手动处理数组索引,只需要关注“哪个项触发了事件”,逻辑更集中,后续加功能(比如批量改所有项样式)也更方便。
原理层面:Vue是怎么实现批量收集ref的?
想深入理解useTemplateRefs,得简单扒一扒Vue的模板编译和ref收集机制。
Vue在编译模板时,会把`ref`属性处理成“给元素/组件绑定ref实例”的逻辑,对于普通ref(单个元素),Vue会生成一个唯一的key,把ref实例存在组件实例的`refs`对象里,而useTemplateRefs的核心,是**让多个同名ref共享同一个收集逻辑**——当模板中有多个`ref="xxx"`时,Vue会把这些ref实例按顺序存到一个数组里,最终通过useTemplateRefs返回的函数,把这个数组暴露给开发者。
换句话说,useTemplateRefs是Vue对“同名ref批量收集”这个场景的官方封装,底层利用了Vue的响应式系统和模板编译时对ref的处理逻辑,帮我们省去了手动管理ref数组的麻烦。
useTemplateRefs是Vue3.3+给开发者的“批量处理模板ref”的利器,在列表渲染、表单管理、动态组件等场景下能大幅简化代码,核心要记住:它帮你自动收集同名ref,调用函数拿数组,注意命名唯一性和调用时机,下次碰到需要批量操作ref的场景,别再手动push数组了,试试useTemplateRefs,效率直接起飞~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。