Vue 模板引用(ref)全面指南:从基础到高级应用

22 阅读5分钟

在 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 的其他特性(如 watchEffectonMounted),构建既保持 Vue 特性又满足特定需求的应用。