Vue3 模板引用
Vue 的声明式写法帮我们避开了大多数 DOM 操作,但总有绕不开的原生场景。
页面加载后让输入框自动聚焦、监听某个元素滚动、量取元素的宽高、调用第三方库初始化……
这时需要拿到真实的 DOM 元素,就要用模板引用Template Ref,简称 ref 的元素版本。
基本用法:useTemplateRef
Vue3.5 起推荐用 useTemplateRef,传入与 ref="名字" 相同的名字,返回一个 ref(初始为 null,元素挂载后才有值):
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 参数名要和模板里的 ref 名一致
const inputRef = useTemplateRef('myInput')
const focusInput = () => {
inputRef.value?.focus()
}
// onMounted 是生命周期钩子,组件挂载到 DOM 后执行一次
// (生命周期钩子后面有专篇,这里先按"挂载后运行"理解)
onMounted(() => {
inputRef.value?.focus() // 页面加载自动聚焦
})
</script>
<template>
<input ref="myInput" type="text" placeholder="自动聚焦" />
<button @click="focusInput">聚焦</button>
</template>
也可以不用 useTemplateRef,直接声明一个同名 ref:给元素 ref="xxx",模板编译时会把 xxx 这个 ref 绑定到元素上。
useTemplateRef 语义更明确、类型推断更好,官方推荐。
模板引用和响应式 ref 冲突吗?名字一样会覆盖吗?
不冲突。计算属性、响应式数据用 ref();元素/组件引用用 useTemplateRef('名字') 或元素 ref="name"。
同名时 <script setup> 顶层会自动处理,但建议约定:数据用 xxxRef 式命名,元素引用用 xxxEl,避免混淆。
在 v-for 中使用
v-for 里的 ref 会得到元素数组,需要在 ref="listItem" 的循环里用 useTemplateRef('listItem'),它返回的就是数组。注意 ref 名和循环变量分开命名,避免混淆:
<script setup>
import { useTemplateRef } from 'vue'
const itemRefs = useTemplateRef('listItem') // ref 名叫 listItem
</script>
<template>
<li v-for="item in list" ref="listItem" :key="item.id">{{ item.name }}</li>
</template>
循环里的 item 是列表数据,ref="listItem" 是元素引用数组,两者不冲突、各司其职。
引用子组件
上面说的都是原生元素。
ref 写在子组件标签上,拿到的不再是 DOM 节点,而是子组件的实例,父组件可以调用子组件暴露的方法(比如重置表单、触发动画)。
子组件的导入和使用在 Vue3 初识组件 讲过:
<!-- 父组件 -->
<script setup>
import { useTemplateRef } from 'vue'
import Child from './Child.vue'
const childRef = useTemplateRef('child')
const callChild = () => {
childRef.value?.sayHello()
}
</script>
<template>
<Child ref="child" />
<button @click="callChild">调子组件方法</button>
</template>
但是,子组件默认不暴露任何内部方法,父组件拿到的实例是”空”的。
要让父组件能调,子组件得用 defineExpose 主动交出方法:
<!-- 子组件 Child.vue -->
<script setup>
import { defineExpose } from 'vue'
const sayHello = () => console.log('你好')
// 只暴露 sayHello,其他内部状态对外不可见
defineExpose({ sayHello })
</script>
<template>子组件内容</template>
不写 defineExpose,父组件就调不到。
这也体现了 Vue 组件的封装原则:只暴露你希望外界使用的接口。
速查卡片
| 场景 | 写法 |
|---|---|
| 原生元素引用 | const el = useTemplateRef('name') + ref="name" |
| 挂载后访问 | 在 onMounted 中读,并用 ?. 防御 null |
| v-for 中的引用 | ref="listItem",返回元素数组 |
| 子组件方法 | 子组件标签 ref + 子组件 defineExpose({ fn }) |
能拿到真实 DOM,前端世界里就没有挡得住的交互了。