关键词:Vue 3.5、Composition API、useTemplateRef、模板引用、TypeScript
1. 开场白:被 ref 支配的恐惧
"哥,我只是想拿到 input 自己 focus 一下,怎么又 undefined 了?"
"你把 ref 名写错了,模板里是 ref="inputEl"
,脚本里写成 inputRef
能行?"
------以上对话每天在全球 420 万个 Vue 组件里循环播放。
Vue 3.0 ~ 3.4 的 ref
在模板引用场景里一直有几个老毛病:
痛点 | 现场翻车 |
---|---|
命名强耦合 | 模板 ref="foo" ↔ 脚本 const foo = ref(null) ,写错一个字母就全剧终 |
类型 = any | 默认推断不出是 HTMLInputElement ,IDE 全程装死 |
动态 ref 几乎无解 | v-for 里 :ref="el-${index}" ,脚本里根本拿不到 |
逻辑抽不出去 | 想在 useFocus() 组合式函数里封装?先学会"影分身" |
于是,Vue 3.5 悄悄塞给我们一个新 API------useTemplateRef
。今天这篇小作文,带你从"以前哭着写"到"现在笑着删"。
2. 以前是怎么"跪着"写代码
2.1 最原始的 ref
vue
<template>
<input ref="username" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 变量名必须和模板完全一致
const username = ref<HTMLInputElement | null>(null)
onMounted(() => {
// 类型收窄全靠 ?
username.value?.focus()
})
</script>
槽点
- 万一模板改名,脚本忘记改------连夜加班。
- 新人不写
<HTMLInputElement | null>
,默认any
一路飘红。
2.2 v-for 里想动态拿 ref?
vue
<template>
<div v-for="(item, i) in list" :key="i">
<input :ref="`input-${i}`" />
</div>
</template>
<script setup>
import { ref } from 'vue'
// 不好意思,官方文档直接劝退:
// "动态 ref 不建议在 <script setup> 里使用"
</script>
得,手写 Map 吧,直接回到 jQuery 时代。
3. Vue 3.5 的"真香"时刻:useTemplateRef 登场
3.1 签名一览
ts
function useTemplateRef<T = Element>(
key: string
): Readonly<Ref<T | null>>
- 参数就是模板里
ref
属性的值; - 返回值是只读 的
Ref
,自动推断类型; - 可在组合式函数里随便调,不依赖组件作用域变量名。
3.2 最简示例
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() // 有提示,有收窄,有幸福
})
</script>
改动点
- 模板
ref="username"
纹丝不动; - 脚本端想叫
inputEl
、el
、foo
随你开心; - 鼠标放上去,IDE 秒出
HTMLInputElement
。
4. 实战:三个高频场景"前后对比"
4.1 场景 A:表单自动聚焦
旧写法 | 新写法 |
---|---|
`const username = ref<HTMLInputElement | null>(null)` |
命名写错 = undefined | 随意命名,零耦合 |
类型手动写 | 全自动推断 |
4.2 场景 B:动态列表里操作某一个 input
vue
<template>
<div v-for="(item, index) in items" :key="index">
<input :ref="`input-${index}`" />
<button @click="focus(index)">Focus</button>
</div>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const focus = (index: number) => {
const el = useTemplateRef<HTMLInputElement>(`input-${index}`)
el.value?.focus()
}
</script>
过去 :得自己维护 refMap: Map<string, El>
,渲染函数里再手动赋值。
现在 :一句 useTemplateRef
搞定,循环 1 万条也不怕。
4.3 场景 C:把"聚焦"逻辑抽到组合式函数
ts
// hooks/useFocus.ts
import { useTemplateRef, nextTick } from 'vue'
export function useFocus(refKey: string) {
const target = useTemplateRef<HTMLInputElement>(refKey)
const focus = () => nextTick(() => target.value?.focus())
return { focus }
}
vue
<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>
旧 API 做不到 :ref
必须声明在组件作用域,组合式函数里无法提前声明变量名 。
新 API :useTemplateRef
只是运行时查 key,彻底解耦。
5. 源码小贴士:为什么它这么快?
Vue 3.5 在编译阶段把模板里的静态 ref
收集到 setupRenderEffect
的 ref
队列里;
useTemplateRef(key)
只是从队列里读对应的 vnode ref ,不触发额外依赖收集,性能 ≈ 原生。
6. 版本 & 兼容
条目 | 要求 |
---|---|
Vue 版本 | ≥ 3.5.0 (2024-09 发布) |
Volar / VSCode 插件 | ≥ 2.1.0 才有类型提示 |
旧项目升级 | 完全向后兼容,可渐进替换 |
7. 踩坑指南
-
别和 v-if 一起乱舞
节点被卸载后
useTemplateRef
返回的value
会重置为null
,记得加?.
。 -
只读!别妄想赋值
tsconst el = useTemplateRef('foo') el.value = document.createElement('div') // ❌ 编译时报错
-
key 必须和模板完全匹配
模板
:ref="input-1"
,脚本里写useTemplateRef('input1')
会找不到。
8. 结论:一句话总结
useTemplateRef
不是银弹,但在"模板引用"这个细分战场,它让命名自由、类型安全、动态 ref、逻辑复用 四连击,写完直接删 30% 防御性代码 ,香到隔壁 React 组都过来问链接。
9. 互动时间
你在哪些奇葩场景里被 ref
坑过?
评论区甩链接,一起给 useTemplateRef
上香~
如果本文帮你少踩一个坑,记得点个 Star 和 赞 哦!