六维教程

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,前端世界里就没有挡得住的交互了。

上一篇
Vue3 动态样式
下一篇
Vue3 生命周期