Vue中为何需要模板引用?又如何高效实现DOM与组件实例的直接访问?

32 阅读16分钟

一、模板引用的基本概念

1.1 什么是模板引用

在Vue的声明式渲染模型中,我们通常不需要直接操作DOM元素——Vue会帮我们处理数据与视图的同步。但在某些场景下,我们还是需要直接访问底层的DOM元素或者子组件实例,这时候就需要用到模板引用(Template Refs)

模板引用通过特殊的ref属性实现,它就像一个“标记”,让我们可以在组件挂载后获取到对应的DOM元素或子组件实例的直接引用。

1.2 为什么需要模板引用

虽然Vue的响应式系统已经能满足大部分开发需求,但以下场景下模板引用会非常有用:

  • 页面加载完成后自动聚焦输入框
  • 初始化第三方DOM库(比如图表、富文本编辑器)
  • 直接操作DOM元素的样式或属性
  • 调用子组件的方法或访问子组件的属性

1.3 应用场景举例

想象一下这样的场景:用户打开一个登录页面,希望输入框自动获得焦点,不需要手动点击。这时候就可以通过模板引用在组件挂载后调用输入框的focus()方法来实现。


二、核心语法与使用方法

2.1 Vue 3.5+ 推荐写法:useTemplateRef

Vue 3.5版本引入了useTemplateRef助手函数,让模板引用的使用更加直观和类型安全。

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 第一个参数必须和模板中的ref属性值完全匹配
const input = useTemplateRef('login-input')

// 组件挂载后执行DOM操作
onMounted(() => {
  // input.value就是对应的DOM元素
  input.value.focus()
})
</script>

<template>
  <!-- 用ref属性标记DOM元素 -->
  <input ref="login-input" placeholder="请输入用户名" />
</template>

代码解析

  1. 使用useTemplateRef('login-input')声明一个引用变量,参数和模板中的ref属性值一致
  2. onMounted钩子中访问input.value,此时组件已经挂载,DOM元素存在
  3. 调用focus()方法让输入框自动获得焦点

2.2 Vue 3.5之前的写法

在Vue 3.5之前,我们需要手动声明一个ref变量,并且变量名要和模板中的ref属性值一致:

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个ref变量,名字必须和模板中的ref属性值匹配
const loginInput = ref(null)

onMounted(() => {
  loginInput.value.focus()
})
</script>

<template>
  <input ref="loginInput" placeholder="请输入用户名" />
</template>

2.3 在v-for中使用模板引用

当在v-for循环中使用模板引用时,对应的ref变量会变成一个数组,包含所有循环生成的DOM元素或组件实例:

<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

const list = ref(['Item 1', 'Item 2', 'Item 3'])
// 使用useTemplateRef获取v-for中的引用数组
const itemRefs = useTemplateRef('list-item')

onMounted(() => {
  console.log(itemRefs.value) // 输出包含所有<li>元素的数组
})
</script>

<template>
  <ul>
    <li v-for="item in list" ref="list-item" :key="item">
      {{ item }}
    </li>
  </ul>
</template>

⚠️ 注意:返回的引用数组不保证和原数组顺序一致,因为Vue的更新策略可能会影响元素的顺序。

往期文章归档
免费好用的热门在线工具

2.4 函数式模板引用

除了字符串形式的ref,我们还可以绑定一个函数来灵活处理引用:

<script setup>
import { ref } from 'vue'

const inputElement = ref(null)

// 自定义函数处理引用
const setInputRef = (el) => {
  inputElement.value = el
}
</script>

<template>
  <!-- 用:ref绑定函数 -->
  <input :ref="setInputRef" placeholder="请输入内容" />
</template>

当元素被挂载时,函数会接收DOM元素作为参数;当元素被卸载时,参数会是null

2.5 组件上的模板引用

模板引用不仅可以用于DOM元素,还可以用于子组件,让我们直接访问子组件的实例:

子组件(Child.vue)

<script setup>
import { ref } from 'vue'

const message = ref('Hello from Child Component!')

const showMessage = () => {
  alert(message.value)
}

// 暴露给父组件的属性和方法
defineExpose({
  message,
  showMessage
})
</script>

父组件

<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'

// 获取子组件实例
const childComponent = useTemplateRef('child')

onMounted(() => {
  // 访问子组件暴露的属性
  console.log(childComponent.value.message) // 输出"Hello from Child Component!"
  // 调用子组件暴露的方法
  childComponent.value.showMessage() // 弹出提示框
})
</script>

<template>
  <Child ref="child" />
</template>

⚠️ 注意:如果子组件使用了<script setup>,默认是私有的,必须通过defineExpose主动暴露属性和方法,父组件才能访问。


三、模板引用工作流程

flowchart LR
    A[模板中定义ref属性] --> B[组件挂载阶段]
    B --> C[Vue创建DOM元素/组件实例]
    C --> D[将引用赋值给对应的ref变量]
    D --> E[组件挂载完成]
    E --> F[可以通过ref.value访问DOM/组件实例]
    F --> G[执行DOM操作或组件交互]

四、课后Quiz

问题1

如何在Vue 3.5+中实现页面加载后自动聚焦输入框?

答案解析: 使用useTemplateRef声明引用,在onMounted钩子中调用focus()方法:

<script setup>
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('auto-focus-input')
onMounted(() => input.value.focus())
</script>
<template>
  <input ref="auto-focus-input" />
</template>

问题2

v-for中使用模板引用时,返回的数组顺序和原数组是否一致?

答案解析: 不一致。Vue的更新策略可能会导致DOM元素的顺序和原数组不同,因此返回的引用数组不保证顺序一致。如果需要保证顺序,建议使用唯一的key值并手动处理。

问题3

当子组件使用<script setup>时,父组件如何通过模板引用访问子组件的属性?

答案解析: 子组件需要使用defineExpose宏主动暴露需要访问的属性和方法,父组件才能通过模板引用访问:

<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
defineExpose({ count })
</script>

<!-- 父组件 -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const child = useTemplateRef('child')
onMounted(() => console.log(child.value.count)) // 输出0
</script>
<template>
  <Child ref="child" />
</template>

五、常见报错解决方案

报错1:Cannot read property 'focus' of null

原因:在组件挂载前访问了模板引用,此时DOM元素还未创建。 解决:将DOM操作放在onMounted钩子中,或者使用watchEffect判断引用是否存在:

watchEffect(() => {
  if (input.value) {
    input.value.focus()
  }
})

报错2:Property 'xxx' does not exist on type 'ComponentPublicInstance<...>'

原因:子组件使用了<script setup>但没有通过defineExpose暴露对应的属性或方法。 解决:在子组件中添加defineExpose暴露需要访问的属性:

<script setup>
const xxx = ref('value')
defineExpose({ xxx })
</script>

报错3:Template ref 'xxx' is not declared with useTemplateRef

原因:Vue 3.5+版本中使用了模板引用但没有用useTemplateRef声明对应的变量。 解决:使用useTemplateRef声明引用变量,确保参数和模板中的ref属性值一致:

const xxx = useTemplateRef('xxx')

参考链接

参考链接:vuejs.org/guide/essen…