4.6 Vue 3 中的模板引用 (Template Refs)
在 Vue 3 中,ref
是一个核心的响应式 API,但它在模板中还有另一个非常重要的用途:获取对 DOM 元素或子组件实例的直接引用。这就是我们所说的“模板引用”。
核心概念
- 目的:让你在父组件中能够直接访问并操作特定的 DOM 元素或子组件实例。
- 场景:
- 手动聚焦一个输入框。
- 触发一个 DOM 元素上的动画。
- 调用子组件暴露的特定方法(非响应式数据)。
- 测量 DOM 元素的尺寸。
- 与需要直接 DOM 访问的第三方库集成。
基本用法
在模板中声明
ref
在你想要引用的元素或组件上,使用
ref
attribute(在 Vue 3 的<script setup>
中,这实际上是一个特殊的指令,但用法像 attribute)。<template><!-- 引用一个 DOM 元素 --><input ref="inputRef" type="text" placeholder="请输入..." /><!-- 引用一个子组件 --><ChildComponent ref="childRef" /> </template>
在
<script setup>
中定义响应式引用使用
ref
函数在<script setup>
中声明一个变量,这个变量的名字必须与模板中ref
attribute 的值完全一致。Vue 会自动将 DOM 元素或组件实例赋值给这个响应式引用。<script setup> import { ref, onMounted } from 'vue' import ChildComponent from './ChildComponent.vue'// 定义响应式引用,名字必须与模板中的 ref 值匹配 const inputRef = ref(null) const childRef = ref(null)// 组件挂载后,引用才可用 onMounted(() => {// 访问 DOM 元素if (inputRef.value) {inputRef.value.focus() // 让输入框自动获得焦点console.log('Input width:', inputRef.value.offsetWidth)}// 调用子组件的方法 (假设子组件暴露了 doSomething 方法)if (childRef.value) {childRef.value.doSomething()} }) </script>
关键要点与注意事项
响应式引用 (
ref
) vs 模板引用 (ref
attribute):ref()
是一个函数,用于创建一个响应式引用对象。这个对象有一个.value
属性,用来存储值(在这里是 DOM 元素或组件实例)。- 模板中的
ref="xxx"
是一个特殊的 attribute,它告诉 Vue 将这个元素/组件的引用注入到名字为xxx
的响应式引用 (ref
) 中。 - 名字必须匹配:
const xxx = ref(null)
和ref="xxx"
中的xxx
必须完全相同。
初始值与访问时机:
- 通常将响应式引用初始化为
null
(const myRef = ref(null)
),因为在组件挂载前,DOM 元素或子组件实例还不存在。 - 在
onMounted
生命周期钩子之前,引用的.value
通常是null
。因为 DOM 渲染发生在onMounted
之后。 - 最佳实践:在
onMounted
或onUpdated
钩子中访问引用,或者在事件处理函数中(确保元素已渲染)。
- 通常将响应式引用初始化为
引用类型:
- DOM 元素:引用
.value
直接指向原生的 DOM 元素对象(如HTMLInputElement
,HTMLDivElement
等),你可以调用其所有原生方法和属性。 - 子组件:引用
.value
指向子组件的实例。你可以访问子组件的公开属性和方法(即在setup
返回或在<script setup>
中用defineExpose
暴露的属性/方法)。
<!-- ChildComponent.vue --> <script setup> import { ref } from 'vue'const count = ref(0)// 暴露给父组件的方法 function increment() {count.value++ }// 明确暴露哪些属性/方法给父组件 defineExpose({increment,// count // 也可以暴露响应式数据,但需谨慎 }) </script>
- DOM 元素:引用
访问子组件的
$el
:- 在 Vue 3 的 Composition API 中,子组件实例本身不直接是 DOM 元素。如果你需要访问子组件的根 DOM 元素,可以通过子组件实例的
$.vnode.el
属性(这是 Vue 内部的,不推荐直接依赖)或者让子组件通过defineExpose
暴露其根元素的引用。
- 在 Vue 3 的 Composition API 中,子组件实例本身不直接是 DOM 元素。如果你需要访问子组件的根 DOM 元素,可以通过子组件实例的
v-for
中的模板引用:- 当
ref
用在v-for
内部的元素或组件上时,对应的引用将是一个包含相应数据的数组,顺序与v-for
渲染的顺序一致。 - 重要:
ref
不会随着v-for
数据的更新而自动同步更新数组。如果数据列表变化(增删改),你需要手动管理这个引用数组,或者考虑使用其他模式(如key
+ 计算属性)。
<template><div v-for="(item, index) in list" :key="item.id" :ref="el => divs[index] = el">{{ item.text }}</div> </template><script setup> import { ref, reactive, onBeforeUpdate } from 'vue'const list = ref([{ id: 1, text: 'A' }, { id: 2, text: 'B' }])// 使用函数式 ref 回调来更灵活地收集引用 const divs = ref([])// 在每次更新前重置引用数组,避免残留 onBeforeUpdate(() => {divs.value = [] }) </script>
- 当
函数式
ref
:- 除了字符串
ref="xxx"
,你还可以传递一个函数:ref="callback"
。这个函数会在每次组件更新时被调用,接收 DOM 元素或组件实例作为参数。这在需要更精细控制引用收集逻辑时非常有用(如上面v-for
的例子)。
<template><input :ref="(el) => inputElement = el" /> </template><script setup> import { ref } from 'vue'const inputElement = ref(null) // 函数内部会设置它// 或者更复杂的逻辑 const setupInputRef = (el) => {if (el) {// 元素被挂载inputElement.value = el// 可以在这里做初始化操作} else {// 元素被卸载inputElement.value = null} } </script>
- 除了字符串
TypeScript 支持:
- 在 TypeScript 中,你可以为模板引用提供精确的类型。
<script setup lang="ts"> import { ref, onMounted } from 'vue' import ChildComponent from './ChildComponent.vue'// 为 DOM 元素引用提供类型 const inputRef = ref<HTMLInputElement | null>(null)// 为子组件引用提供类型 (需要导入组件类型) const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)onMounted(() => {if (inputRef.value) {inputRef.value.focus() // TypeScript 知道这是 HTMLInputElement}if (childRef.value) {childRef.value.increment() // TypeScript 知道可以调用 increment} }) </script>
Vue 3 的模板引用 (ref
) 是一个强大且常用的特性,用于在父组件中直接操作 DOM 或子组件。核心是:
- 在模板中使用
ref="myName"
。 - 在
<script setup>
中使用const myName = ref(null)
定义响应式引用。 - 在
onMounted
或之后访问myName.value
来获取 DOM 元素或组件实例。 - 对于子组件,使用
defineExpose
来控制暴露的 API。 - 在
v-for
中使用时,引用是数组,需注意更新时机。 - 在 TypeScript 中提供精确类型。
原则:尽量通过响应式数据和 props/events 来进行组件通信,仅在确实需要直接 DOM 操作或调用特定方法时才使用模板引用。