一、前端操作 DOM 的肌肉记忆
在 HTML、CSS、JS 操作 DOM 的那个年代,我已经对 JS 操作 DOM 形成了"肌肉记忆"。
正常的前端思维是这样的:给 DOM 元素一个 class 类名或者 id,然后通过 document.querySelector() 拿到这个 DOM 元素。
<divclass="hello">hello,world</div><script setup>import{onMounted}from'vue'onMounted(()=>{constdom=document.querySelector('.hello')console.log(dom)// DOMElement})</script>不用声明什么、不用初始化、名字写错也只是拿到 null——一切都符合直觉。
二、Vue 3.5 之前:字符串 ref 的三宗罪
然而在 Vue 3.5 之前,如果想拿到一个真实的 DOM 元素,就有点"反前端"的感觉了。
罪一:还没开始拿 DOM,先得让变量等于 null
<div ref="helloRef">hello, world</div> <script setup> import { onMounted, ref } from 'vue' const helloRef = ref(null) onMounted(() => { console.log(helloRef.value) // 到这里才终于拿到 DOM }) </script>还没开始获取 DOM 呢,先让 ref 为 null——这个操作就有点无语。
罪二:TypeScript 的类型要自己猜、自己写
如果项目用了 TypeScript,helloRef到底是个什么类型?编译器不知道,你只能手动补一个泛型:
consthelloRef=ref<HTMLElement|null>(null)关键是——你得先猜对。猜成HTMLElement,结果模板里绑的是组件,运行时才发现不对;猜成any,那等于放弃了类型检查。类型这件事明明可以从模板里读出来,却要求你手写一遍。
罪三:名字必须和模板对齐,对不上还不报错
这一条最隐蔽,也最容易让人debug 到怀疑人生。
<template> <!-- 模板里叫 el --> <div ref="el">hello, world</div> </template> <script setup> import { ref } from 'vue' // 但变量叫 inputRef —— 完蛋 const inputRef = ref(null) onMounted(() => { console.log(inputRef.value) // null,永远是 null }) </script>变量名和模板里的 ref 字符串对不上时,setup 里声明的这个 ref根本不会被赋值,.value永远是null,而且不会有任何报错提示。
前端写querySelector('.hello')的时候,至少知道自己在找什么;而 Vue 这里,我只是在跟一个字符串对齐——字符串拼错了,编译器也不拦你。
三、useTemplateRef:把这套别扭的流程收回来
Vue 3.5带来了useTemplateRef,写法是这样的:
<div ref="helloRef">hello, world</div> <script setup> import { onMounted, useTemplateRef } from 'vue' const helloRef = useTemplateRef('helloRef') onMounted(() => { console.log(helloRef.value) // DOMElement }) </script>终于开始符合老前端的思维了:
- 先给 DOM 元素绑定一个 ref 字符串(
ref="helloRef") - 通过
useTemplateRef声明这个 ref,直接拿到那个 DOM 元素
不用再ref(null)、不用再手写泛型、变量名和字符串不一致的问题也彻底消失——因为helloRef本身就是变量,字符串只作为参数传进去,两者天然对得上。
四、最被低估的价值:TypeScript 类型自动推断
上面那还只是"写法变简洁了"。真正让useTemplateRef值得单独写一篇的,是它在TypeScript 下的类型推断。
// Vue 3.5 之前 —— 类型得自己猜、自己写consthelloRef=ref<HTMLElement|null>(null)// Vue 3.5 之后 —— 类型从模板自动推断consthelloRef=useTemplateRef('helloRef')helloRef.value?.focus()// ✅ 编辑器知道这是 HTMLElementhelloRef.value.innerHTML// ✅ 属性、DOM API 全都有补全编译器知道模板里ref="helloRef"绑的是哪个元素,于是直接把类型传回来。我也终于不需要写任何泛型,也不需要any。
这解决的不只是"少写几个字",而是把"类型信息的所有权"还给了模板——模板里写了什么,类型就是什么,两者永远一致。
五、两个你可能不知道的点
1. 它是响应式的,所以可以直接 watch
useTemplateRef返回的是一个真实的 ref,所以可以直接监听:
watch(helloRef,(el)=>{el?.focus()// 挂载完成的那一刻自动触发})在 3.5 之前,ref(null)里的null → DOMElement这个变化同样能被 watch 到,但很多人不知道,以为 ref 就是个"装 DOM 的盒子"。实际上它是个信号。
2. 它是 readonly 的
helloRef.value=someOtherEl// ❌ 不允许这是故意的——你不能手动往里塞元素。想让 DOM 变化,唯一的路径是"改数据 → 视图更新"。这恰好是 Vue 想要的正道:DOM 是数据渲染出来的结果,不是一个可以随手拨弄的对象。
六、为什么要写这种文章
AI 发展太快了。我现在每天的工作就是让 AI 给我干活…但是 AI 写的对与不对我也不怎么关注了,我只关注到业务有没有帮我实现。
与其焦虑,不如把每天看到的小知识点记下来——这是我能给自己的安慰。
我感觉我已经什么都不知道了,所以这属于备忘。写下来。