"哥,我只是想拿到input自己focus一下,怎么又undefined了?"
"你把ref名写错了,模板里是
ref="inputEl"
,脚本里写成inputRef
能行?"------每天在Vue项目里循环播放的对话,现在终于有了终结方案。

Vue 3.5版本发布一个名为useTemplateRef
的新API悄然登场。这个看似不起眼的"小补丁",却让全球420万Vue开发者沸腾------终于不用再为模板ref的命名耦合、类型缺失、动态获取而头疼了!
传统ref的四大"血泪痛点"
在Vue 3.0到3.4版本中,开发者处理模板引用时,几乎都踩过这四个坑:
命名强耦合 :模板里写ref="username"
,脚本里必须声明const username = ref(null)
,变量名和ref值必须完全一致。一旦模板改名脚本没改,控制台立刻报undefined
,排查起来像大海捞针。
类型安全缺失 :默认情况下,ref变量会被推断为any
类型,IDE无法提供DOM方法提示。想获得类型支持?必须手动写ref<HTMLInputElement | null>(null)
,多写15个字符不说,新人还经常漏写导致类型失效。
动态ref难题 :在v-for
中生成动态ref(如:ref="input-${index}"
)时,传统方式需要手动维护一个ref映射表,写一堆if-else
判断,代码量激增3倍,还容易出错。
逻辑复用困难:想把"输入框聚焦"这样的逻辑封装到组合式函数?传统ref必须在组件内声明,根本无法抽离到外部hooks,导致代码重复率飙升。
正如掘金网友@前端老司机吐槽:"为了在v-for里拿个input的ref,我写了20行映射逻辑,结果上线后发现key值没对齐,直接被测试怼到怀疑人生。"
useTemplateRef:四大核心功能彻底重构模板引用
Vue 3.5新增的useTemplateRef
API,通过四个关键特性直击痛点:
命名解耦:变量名与ref值零绑定
传统ref要求变量名必须与模板ref值完全一致,而useTemplateRef
通过key关联------模板里写ref="username"
,脚本里可以叫inputEl
、el
甚至foo
,彻底摆脱命名枷锁。
类型自动推断:IDE提示秒出
无需手动声明类型,useTemplateRef
会根据DOM元素自动推断类型。鼠标悬停变量,IDE立刻显示HTMLInputElement | null
,focus()
、value
等方法属性一目了然,告别"盲写"时代。
动态ref支持:一行代码搞定v-for场景
在v-for
中动态生成ref时,只需通过模板字符串拼接key(如input-${index}
),再用useTemplateRef
传入对应key即可直接获取元素,省去手动维护映射表的繁琐步骤。
逻辑可复用:组合式函数封装DOM操作
useTemplateRef
支持在组合式函数中通过ref key获取元素,轻松将"聚焦"、"滚动"等DOM操作逻辑抽离成hooks,复用率提升50%以上。
实战对比:传统ref vs useTemplateRef
基础用法:从"命名绑定"到"自由命名"
传统ref方式:
vue
<template>
<input ref="username" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 变量名必须与模板ref值完全一致
const username = ref<HTMLInputElement | null>(null)
onMounted(() => {
username.value?.focus() // 类型需手动声明,否则为any
})
</script>
useTemplateRef方式:
vue
<template>
<input ref="username" />
</template>
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// 变量名随意,类型自动推断为HTMLInputElement | null
const inputEl = useTemplateRef('username')
onMounted(() => {
inputEl.value?.focus() // IDE直接提示focus()方法
})
</script>
差异一目了然:变量名从"被迫一致"变为"完全自由",类型声明从"手动书写"变为"自动推断",代码量减少20%。
动态列表:从"20行映射"到"1行获取"
传统ref方式(需手动维护映射表):
vue
<template>
<div v-for="(item, i) in list" :key="i">
<input :ref="`input-${i}`" />
</div>
</template>
<script setup>
import { ref } from 'vue'
// 手动创建映射表
const refs = ref<Map<number, HTMLInputElement>>(new Map())
// 复杂的赋值逻辑
const setRef = (el: HTMLInputElement, index: number) => {
if (el) refs.value.set(index, el)
}
</script>
useTemplateRef方式(直接通过key获取):
vue
<template>
<div v-for="(item, index) in items" :key="index">
<input :ref="`input-${index}`" />
<button @click="focus(index)">聚焦</button>
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const focus = (index: number) => {
// 直接通过动态key获取ref,无需映射表
const el = useTemplateRef<HTMLInputElement>(`input-${index}`)
el.value?.focus()
}
</script>
效率碾压:动态ref处理从"声明映射表+赋值逻辑"的多步操作,简化为"key拼接+直接获取"的一行代码,逻辑复杂度降低80%。
逻辑复用:从"组件内硬写"到"hooks封装"
传统ref方式(无法抽离逻辑):
vue
<template>
<input ref="email" />
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
const email = ref<HTMLInputElement | null>(null)
// 聚焦逻辑只能写在组件内,无法复用
const focusEmail = () => nextTick(() => {
email.value?.focus()
})
</script>
useTemplateRef方式(抽离为组合式函数):
typescript
// hooks/useFocus.ts
import { useTemplateRef, nextTick } from 'vue'
// 封装聚焦逻辑为通用hooks
export function useFocus(refKey: string) {
const target = useTemplateRef<HTMLInputElement>(refKey)
const focus = () => nextTick(() => target.value?.focus())
return { focus }
}
// 组件中使用
<template>
<input ref="email" />
<button @click="focus">Focus Email</button>
</template>
<script setup lang="ts">
import { useFocus } from '@/hooks/useFocus'
const { focus } = useFocus('email') // 一行引入复用逻辑
</script>
复用革命 :通过useTemplateRef
,DOM操作逻辑从"组件内硬编码"升级为"跨组件复用hooks",一个项目中重复代码减少上千行。
避坑指南:useTemplateRef的3个关键限制
虽然useTemplateRef
强大,但使用时需注意三个"雷区":
1. v-if隐藏元素时,value会变为null
当元素被v-if
条件隐藏,useTemplateRef
返回的ref.value会重置为null。操作前务必用?.
或条件判断:
typescript
const el = useTemplateRef('target')
// 安全操作方式
el.value?.classList.add('active')
// 或
if (el.value) {
el.value.classList.add('active')
}
2. 返回的ref是只读的,无法手动赋值
useTemplateRef
返回的是只读Ref对象,手动修改value会直接报错:
typescript
const el = useTemplateRef('target')
el.value = document.createElement('div') // ❌ 编译时报错:无法分配到 "value" ,因为它是只读属性
3. key必须与模板ref值完全匹配
模板ref值与useTemplateRef
的key参数必须大小写、空格完全一致,否则无法获取元素:
vue
<template>
<input ref="Email" /> <!-- 模板ref是"Email"(大写E) -->
</template>
<script setup>
const email = useTemplateRef('email') // ❌ key是"email"(小写e),匹配失败
</script>
最佳实践:写出优雅的ref引用代码
1. 显式标注类型,提升可读性
虽然useTemplateRef
支持自动类型推断,但显式标注类型能让代码更清晰:
typescript
// 推荐:显式标注类型
const username = useTemplateRef<HTMLInputElement>('username')
// 不推荐:依赖自动推断(虽能工作,但可读性差)
const username = useTemplateRef('username')
2. 集中管理ref key,避免硬编码
复杂组件中,将ref key统一存放在常量文件,防止拼写错误:
typescript
// constants/refKeys.ts
export const REF_KEYS = {
USERNAME_INPUT: 'username',
EMAIL_INPUT: 'email'
}
// 组件中使用
import { REF_KEYS } from '@/constants/refKeys'
const username = useTemplateRef<HTMLInputElement>(REF_KEYS.USERNAME_INPUT)
3. 配合nextTick,确保DOM已更新
DOM更新后立即操作元素时,用nextTick
保证元素已挂载:
typescript
import { useTemplateRef, nextTick } from 'vue'
const submitBtn = useTemplateRef<HTMLButtonElement>('submitBtn')
const handleSubmit = async () => {
await submitForm() // 执行DOM更新操作
nextTick(() => {
submitBtn.value?.classList.add('success') // 确保DOM已更新
})
}
4. 封装通用hooks,最大化复用
将高频DOM操作(如滚动、自动保存)封装为hooks,提升项目一致性:
typescript
// hooks/useInputAutoSave.ts
import { useTemplateRef, watch, nextTick } from 'vue'
// 输入框自动保存hook
export function useInputAutoSave(
refKey: string,
saveFn: (value: string) => void,
delay = 1000
) {
const input = useTemplateRef<HTMLInputElement>(refKey)
watch(
() => input.value?.value,
(newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
const timer = setTimeout(() => {
saveFn(newVal)
clearTimeout(timer)
}, delay)
}
}
)
return { input }
}
总结
随着Vue 3.5的普及,useTemplateRef
有望成为模板引用的首选方案。现在就升级Vue版本,体验这场"ref革命"吧------毕竟,没有哪个开发者愿意再写"变量名必须和ref值一致"的重复代码了。
注意:使用前请确保项目已升级到Vue 3.5.0+,并安装Volar 2.1.0+以获得完整类型支持。
(你在项目中被传统ref坑过吗?欢迎在评论区分享你的"血泪史"!)
已同步至微信公众号《前端日月潭》