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前端网发表,如需转载,请注明页面地址。
code前端网




发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。