在 Vue 开发中,虽然声明性渲染模型为我们抽象了大部分 DOM 操作,但在某些场景下,我们仍然需要直接访问底层 DOM 元素或子组件实例。Vue 提供的 ref
attribute 正是为解决这类需求而生,它允许我们在元素或组件挂载后获取其直接引用,实现更精细的控制。本文将深入探讨 Vue 模板引用的各种用法与最佳实践。
一、ref 的基本概念与核心作用
ref
是 Vue 中一个特殊的 attribute,类似于 v-for
中的 key
,其核心功能是为 DOM 元素或子组件实例创建引用标识。当元素或组件被挂载到 DOM 后,我们可以通过这些引用执行以下操作:
-
对 DOM 元素进行焦点设置、动画控制等底层操作
-
初始化或操作第三方库(如 Chart.js 图表实例)
-
访问子组件的实例方法与属性
-
在
v-for
循环中批量获取元素引用
关键特性:
- 引用仅在组件挂载后可用(初次渲染前为
null
) - 支持字符串命名与函数绑定两种方式
- 在组合式 API 中通过
useTemplateRef
函数获取引用 - 子组件引用的访问受
<script setup>
私有性限制
二、在组合式 API 中使用 ref
2.1 获取 DOM 元素引用
在组合式 API 中,获取 DOM 元素引用的标准流程如下:
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 声明与模板中 ref 值匹配的引用
const inputRef = useTemplateRef('input-element')
// 组件挂载后访问引用
onMounted(() => {
// 对输入框设置焦点
inputRef.value.focus()
// 操作 DOM 元素属性
inputRef.value.placeholder = '已通过 ref 初始化'
})
</script>
<template>
<input ref="input-element" type="text" />
</template>
2.2 TypeScript 类型推断
Vue 对 TypeScript 提供了良好支持,inputRef.value
的类型会根据匹配的元素自动推断:
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// TypeScript 自动推断 inputRef.value 为 HTMLInputElement 类型
const inputRef = useTemplateRef('input-element')
onMounted(() => {
// 类型安全的操作
inputRef.value.addEventListener('input', (e) => {
console.log(e.target.value)
})
})
</script>
三、模板引用的高级使用场景
3.1 监听引用变化
由于引用在组件挂载前为 null
,且可能随组件卸载而消失,监听时需进行非空判断:
js
import { useTemplateRef, watchEffect } from 'vue'
const inputRef = useTemplateRef('input-element')
// 响应式监听引用变化
watchEffect(() => {
if (inputRef.value) {
// 元素已挂载,执行操作
inputRef.value.style.border = '2px solid blue'
} else {
// 元素未挂载或已卸载
console.log('元素状态变更,当前为 null')
}
})
3.2 子组件引用与组件通信
当 ref
应用于子组件时,引用值为子组件实例,可访问其属性与方法:
<!-- 父组件 -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 获取子组件实例引用
const childRef = useTemplateRef('child')
onMounted(() => {
// 调用子组件方法
childRef.value.doSomething()
// 访问子组件属性
console.log(childRef.value.dataValue)
})
</script>
<template>
<ChildComponent ref="child" />
</template>
<!-- 子组件(选项式 API) -->
<script>
export default {
data() {
return {
dataValue: '子组件数据'
}
},
methods: {
doSomething() {
console.log('子组件方法被调用')
}
}
}
</script>
3.3 <script setup>
组件的引用限制与暴露
使用 <script setup>
的组件默认私有,父组件无法直接访问其属性,需通过 defineExpose
显式暴露:
<!-- 子组件(<script setup>) -->
<script setup>
import { ref } from 'vue'
// 私有属性
const privateData = '不会被暴露'
const exposedData = ref('将被暴露的数据')
// 暴露属性与方法
defineExpose({
exposedData,
// ref 会自动解包为原始值
getExposedValue() {
return exposedData.value
}
})
</script>
<template>
<div>子组件内容</div>
</template>
<!-- 父组件访问暴露的子组件引用 -->
<script setup>
import { useTemplateRef } from 'vue'
import ExposedChild from './ExposedChild.vue'
const childRef = useTemplateRef('exposedChild')
// 组件挂载后访问暴露的属性
console.log(childRef.value.exposedData) // 输出: "将被暴露的数据"
console.log(childRef.value.getExposedValue()) // 输出: "将被暴露的数据"
</script>
四、v-for 中的模板引用(v3.5+)
Vue 3.5 及以上版本支持在 v-for
中获取元素引用数组,这在批量操作元素时非常实用:
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const items = ref([
{ id: 1, name: '项目一' },
{ id: 2, name: '项目二' },
{ id: 3, name: '项目三' }
])
// 获取 v-for 中所有 li 元素的引用数组
const itemRefs = useTemplateRef('listItems')
onMounted(() => {
// itemRefs.value 是包含所有 li 元素的数组
console.log('元素数量:', itemRefs.value.length)
// 批量设置样式
itemRefs.value.forEach((el, index) => {
el.style.color = index % 2 === 0 ? 'red' : 'blue'
})
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id" ref="listItems">
{{ item.name }}
</li>
</ul>
</template>
注意事项:
- ref 数组顺序不一定与源数组完全一致
- 元素卸载时,对应引用会从数组中移除
- 可结合
key
提升引用匹配的稳定性
五、函数式模板引用
除了字符串命名,ref
还可绑定为函数,在元素挂载 / 更新 / 卸载时触发:
<script setup>
import { ref } from 'vue'
// 声明 ref 变量存储元素引用
const inputEl = ref(null)
// 函数式 ref 绑定
const setInputRef = (el) => {
// 元素挂载时 el 为 DOM 实例,卸载时为 null
inputEl.value = el
if (el) {
// 元素已挂载,初始化操作
el.placeholder = '函数式 ref 绑定'
}
}
</script>
<template>
<!-- 使用 v-bind:ref 绑定函数 -->
<input v-bind:ref="setInputRef" type="text" />
<!-- 或使用内联函数 -->
<input :ref="(el) => { if (el) el.focus() }" />
</template>
函数式 ref 的优势:
- 更灵活的引用赋值逻辑
- 可在元素卸载时执行清理操作
- 适合动态绑定场景(如条件渲染元素)
六、注意事项
6.1 避免滥用 ref
- 优先使用声明式编程:Vue 的核心优势在于声明式渲染,应尽量通过数据驱动视图,而非直接操作 DOM
- 组件通信首选 props/emit:父子组件交互优先使用标准接口,仅在必要时使用 ref 访问子组件
- 第三方库集成场景:当需要操作库实例(如表单验证库、图表库)时,ref 是合理的选择
6.2 性能考虑
- 批量操作 DOM:使用
nextTick
确保在 DOM 更新完成后执行批量操作 - 避免频繁访问 ref:在循环或高频事件中,可先缓存
ref.value
以提升性能 - v-for 中慎用函数式 ref:函数式 ref 在每次更新时都会被调用,可能影响性能
6.3 类型安全(TypeScript)
- 显式标注类型:在复杂场景中,可通过泛型为
useTemplateRef
标注精确类型 - 使用
defineRef
与ref
:确保 ref 变量的类型推导正确 - 参考官方类型声明:查看 Vue 类型定义文件,了解内置类型的使用方式
七、总结
ref
作为 Vue 中操作底层元素的重要接口,在保持声明式编程优势的同时,为开发者提供了必要的命令式控制能力。从基础的 DOM 焦点设置,到复杂的子组件通信与第三方库集成,ref 的灵活用法贯穿于各类开发场景。
在实际项目中,建议遵循 "声明式优先,命令式保底" 的原则,合理使用 ref 并结合组合式 API 的其他特性(如 watchEffect
、onMounted
),构建既保持 Vue 特性又满足特定需求的应用。