Vue 3 模板引用与$refs
1. 模板引用概述
模板引用(Template Refs)是Vue中用于直接访问DOM元素或组件实例的机制。在Vue 3中,模板引用可以通过ref函数结合模板中的ref属性来使用,它提供了一种安全、类型安全的方式来访问DOM元素和组件实例。
1.1 为什么需要模板引用
虽然Vue鼓励使用声明式编程,但在某些情况下,我们需要直接访问DOM元素或组件实例,例如:
- 操作DOM元素,如获取元素尺寸、滚动位置等
- 调用组件实例的方法
- 集成第三方库,如Chart.js、D3.js等
- 实现复杂的交互效果,如拖拽、动画等
1.2 Vue 3模板引用的特点
Vue 3的模板引用具有以下特点:
- 组合式API优先:在组合式API中使用更灵活、更强大
- 响应式支持:模板引用是响应式的,当引用的元素或组件发生变化时,引用会自动更新
- 类型安全:在TypeScript环境下,支持类型推断和类型检查
- 支持多个引用:可以在同一个组件中使用多个模板引用
- 支持动态引用:可以根据条件动态创建模板引用
2. 模板引用的基本使用
2.1 基本语法
在组合式API中,模板引用的基本语法如下:
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个模板引用
const inputRef = ref(null)
onMounted(() => {
// 在组件挂载后,可以访问DOM元素
inputRef.value.focus()
})
return {
inputRef // 将引用暴露给模板
}
}
}在模板中,使用ref属性将DOM元素或组件实例绑定到模板引用:
<template>
<input type="text" ref="inputRef" placeholder="Focus me!" />
</template>2.2 访问DOM元素
使用模板引用可以直接访问DOM元素,并调用其方法或访问其属性:
import { ref, onMounted } from 'vue'
export default {
setup() {
const divRef = ref(null)
const inputRef = ref(null)
onMounted(() => {
// 获取div元素的尺寸
const { offsetWidth, offsetHeight } = divRef.value
console.log(`Div尺寸: ${offsetWidth}x${offsetHeight}`)
// 调用input元素的focus方法
inputRef.value.focus()
// 修改input元素的属性
inputRef.value.placeholder = 'Focused!'
})
return {
divRef,
inputRef
}
}
}在模板中:
<template>
<div ref="divRef" style="width: 200px; height: 100px; background-color: #f0f0f0;">
<p>Div Element</p>
</div>
<input type="text" ref="inputRef" placeholder="Focus me!" />
</template>2.3 访问组件实例
使用模板引用也可以访问子组件的实例,并调用其方法或访问其属性:
// 子组件
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment // 暴露方法给父组件
}
}
}
// 父组件
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
setup() {
const childRef = ref(null)
onMounted(() => {
// 访问子组件的属性
console.log('子组件count初始值:', childRef.value.count)
// 调用子组件的方法
childRef.value.increment()
console.log('子组件count调用后:', childRef.value.count)
})
return {
childRef
}
}
}在模板中:
<template>
<ChildComponent ref="childRef" />
</template>3. 模板引用的高级使用
3.1 多个模板引用
可以在同一个组件中使用多个模板引用,每个引用对应不同的DOM元素或组件实例:
import { ref, onMounted } from 'vue'
export default {
setup() {
const headerRef = ref(null)
const contentRef = ref(null)
const footerRef = ref(null)
onMounted(() => {
// 访问多个DOM元素
console.log('Header高度:', headerRef.value.offsetHeight)
console.log('Content高度:', contentRef.value.offsetHeight)
console.log('Footer高度:', footerRef.value.offsetHeight)
})
return {
headerRef,
contentRef,
footerRef
}
}
}在模板中:
<template>
<header ref="headerRef">
<h1>Page Header</h1>
</header>
<main ref="contentRef">
<p>Page Content</p>
</main>
<footer ref="footerRef">
<p>Page Footer</p>
</footer>
</template>3.2 动态模板引用
可以根据条件动态创建模板引用,例如在v-for循环中:
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个数组来存储多个引用
const itemRefs = ref([])
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']
onMounted(() => {
// 访问第一个item的DOM元素
itemRefs.value[0]?.style.backgroundColor = 'red'
})
const highlightItem = (index) => {
// 重置所有item的背景色
itemRefs.value.forEach((item) => {
if (item) {
item.style.backgroundColor = ''
}
})
// 高亮指定的item
if (itemRefs.value[index]) {
itemRefs.value[index].style.backgroundColor = 'yellow'
}
}
return {
items,
itemRefs,
highlightItem
}
}
}在模板中:
<template>
<div>
<div
v-for="(item, index) in items"
:key="index"
:ref="el => itemRefs[index] = el"
@click="highlightItem(index)"
style="padding: 10px; margin: 5px; border: 1px solid #ccc; cursor: pointer;"
>
{{ item }}
</div>
</div>
</template>3.3 组件卸载时的处理
当组件卸载时,模板引用会自动设置为null,这可以避免内存泄漏:
import { ref, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const timerRef = ref(null)
const divRef = ref(null)
onMounted(() => {
// 启动定时器,每隔1秒更新div的内容
timerRef.value = setInterval(() => {
if (divRef.value) {
divRef.value.textContent = `Current time: ${new Date().toLocaleTimeString()}`
}
}, 1000)
})
onUnmounted(() => {
// 清理定时器
if (timerRef.value) {
clearInterval(timerRef.value)
}
})
return {
divRef
}
}
}4. 模板引用与TypeScript
在TypeScript环境下,模板引用支持类型推断和类型检查,可以提供更好的开发体验和类型安全。
4.1 基本类型支持
import { ref, onMounted } from 'vue'
export default {
setup() {
// 指定模板引用的类型为HTMLInputElement
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// 类型安全的访问
inputRef.value?.focus()
inputRef.value?.select()
})
return {
inputRef
}
}
}4.2 组件类型支持
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
setup() {
// 指定模板引用的类型为ChildComponent
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)
onMounted(() => {
// 类型安全的访问组件属性和方法
if (childRef.value) {
console.log(childRef.value.count)
childRef.value.increment()
}
})
return {
childRef
}
}
}4.3 动态模板引用的类型支持
import { ref, onMounted } from 'vue'
export default {
setup() {
// 指定动态模板引用的类型为HTMLDivElement数组
const itemRefs = ref<HTMLDivElement[]>([])
const items = ['Item 1', 'Item 2', 'Item 3']
onMounted(() => {
// 类型安全的访问数组中的元素
itemRefs.value[0]?.style.backgroundColor = 'red'
})
return {
items,
itemRefs
}
}
}5. 模板引用的最佳实践
5.1 只在必要时使用
虽然模板引用很强大,但也不要过度使用,否则会导致以下问题:
- 违背声明式编程原则:直接操作DOM会使代码更难维护和测试
- 耦合度高:组件与DOM元素或子组件的实现细节耦合在一起
- 性能问题:频繁操作DOM可能会导致性能问题
建议只在以下情况下使用模板引用:
- 操作DOM元素,如获取元素尺寸、滚动位置等
- 调用组件实例的方法
- 集成第三方库
- 实现复杂的交互效果
5.2 在onMounted钩子中访问
模板引用只有在组件挂载后才能访问,因此建议在onMounted钩子中访问模板引用:
// 好的做法:在onMounted钩子中访问
onMounted(() => {
inputRef.value?.focus()
})
// 不好的做法:在setup函数中直接访问
// inputRef.value?.focus() // 这里inputRef.value是null5.3 检查引用是否存在
在访问模板引用之前,总是检查引用是否存在,以避免空指针异常:
// 好的做法:检查引用是否存在
if (inputRef.value) {
inputRef.value.focus()
}
// 好的做法:使用可选链操作符
inputRef.value?.focus()
// 不好的做法:直接访问,可能导致空指针异常
// inputRef.value.focus() // 如果inputRef.value是null,会报错5.4 清理资源
在组件卸载前,清理使用模板引用创建的资源,如事件监听器、定时器等:
import { ref, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const divRef = ref(null)
let resizeObserver = null
onMounted(() => {
// 创建ResizeObserver
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
console.log('Div尺寸变化:', entry.contentRect.width, entry.contentRect.height)
})
// 观察div元素
if (divRef.value) {
resizeObserver.observe(divRef.value)
}
})
onUnmounted(() => {
// 清理ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect()
}
})
return {
divRef
}
}
}5.5 结合组合式函数使用
可以将模板引用相关的逻辑封装到组合式函数中,提高代码复用性:
// useResizeObserver.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useResizeObserver() {
const elementRef = ref(null)
const size = ref({ width: 0, height: 0 })
let resizeObserver = null
onMounted(() => {
resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
size.value = {
width: entry.contentRect.width,
height: entry.contentRect.height
}
})
if (elementRef.value) {
resizeObserver.observe(elementRef.value)
}
})
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
}
})
return {
elementRef,
size
}
}
// 在组件中使用
import { useResizeObserver } from './useResizeObserver'
export default {
setup() {
const { elementRef, size } = useResizeObserver()
return {
elementRef,
size
}
}
}在模板中:
<template>
<div ref="elementRef" style="width: 200px; height: 100px; background-color: #f0f0f0;">
<p>Width: {{ size.width }}px</p>
<p>Height: {{ size.height }}px</p>
</div>
</template>6. 模板引用与选项式API的对比
| 特性 | 组合式API | 选项式API |
|---|---|---|
| 语法 | 使用ref函数和模板ref属性 |
使用this.$refs对象 |
| 响应式 | 响应式,自动更新 | 非响应式,需要手动更新 |
| 类型安全 | 支持TypeScript类型推断和检查 | 类型安全较差 |
| 动态引用 | 支持,使用箭头函数 | 不支持,只能使用字符串 |
| 访问时机 | 只能在组件挂载后访问 | 可以在mounted钩子或之后访问 |
| 多个引用 | 支持,使用数组或对象 | 支持,使用对象 |
7. 模板引用的性能优化
7.1 避免在渲染函数中使用模板引用
避免在渲染函数中使用模板引用,因为这会导致每次渲染都创建新的引用:
// 好的做法:在setup函数中创建模板引用
const divRef = ref(null)
// 不好的做法:在渲染函数中创建模板引用
return () => h('div', { ref: el => divRef.value = el }, 'Content')7.2 只在必要时更新引用
避免频繁更新模板引用,因为这会导致不必要的性能开销:
// 好的做法:只在元素变化时更新引用
:ref="el => itemRefs[index] = el"
// 不好的做法:每次渲染都更新引用
:ref="el => {
// 复杂的逻辑,每次渲染都执行
itemRefs[index] = el
console.log('Ref updated')
}"7.3 使用shallowRef
对于不需要响应式的模板引用,可以使用shallowRef来提高性能:
// 好的做法:使用shallowRef
const inputRef = shallowRef(null)
// 不好的做法:使用ref
const inputRef = ref(null)8. 模板引用的高级应用
8.1 集成第三方库
使用模板引用集成第三方库,如Chart.js:
import { ref, onMounted, onUnmounted } from 'vue'
import Chart from 'chart.js/auto'
export default {
setup() {
const canvasRef = ref(null)
let chart = null
onMounted(() => {
// 创建Chart实例
if (canvasRef.value) {
chart = new Chart(canvasRef.value, {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
})
}
})
onUnmounted(() => {
// 销毁Chart实例
if (chart) {
chart.destroy()
}
})
return {
canvasRef
}
}
}在模板中:
<template>
<canvas ref="canvasRef" width="400" height="400"></canvas>
</template>8.2 实现拖拽功能
使用模板引用实现拖拽功能:
import { ref, onMounted, onUnmounted } from 'vue'
export default {
setup() {
const dragRef = ref(null)
let isDragging = false
let startX = 0
let startY = 0
let initialX = 0
let initialY = 0
const handleMouseDown = (e) => {
isDragging = true
startX = e.clientX
startY = e.clientY
initialX = dragRef.value.offsetLeft
initialY = dragRef.value.offsetTop
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
const handleMouseMove = (e) => {
if (!isDragging) return
const dx = e.clientX - startX
const dy = e.clientY - startY
dragRef.value.style.left = `${initialX + dx}px`
dragRef.value.style.top = `${initialY + dy}px`
}
const handleMouseUp = () => {
isDragging = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
onMounted(() => {
// 设置初始样式
if (dragRef.value) {
dragRef.value.style.position = 'absolute'
dragRef.value.style.left = '100px'
dragRef.value.style.top = '100px'
}
})
onUnmounted(() => {
// 清理事件监听器
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
})
return {
dragRef,
handleMouseDown
}
}
}在模板中:
<template>
<div
ref="dragRef"
@mousedown="handleMouseDown"
style="width: 100px; height: 100px; background-color: red; cursor: move;"
>
Drag me!
</div>
</template>9. 实战练习
练习1:使用模板引用获取元素尺寸
创建一个组件,使用模板引用获取DOM元素的尺寸,并在页面上显示:
- 创建一个div元素,设置固定的宽度和高度
- 使用模板引用获取该div元素的实际尺寸
- 在页面上显示元素的宽度和高度
练习2:使用模板引用调用组件方法
创建一个父组件和一个子组件,父组件使用模板引用调用子组件的方法:
- 子组件包含一个计数器和一个increment方法
- 父组件使用模板引用获取子组件实例
- 父组件调用子组件的increment方法,并显示子组件的计数器值
练习3:使用模板引用集成第三方库
使用模板引用集成第三方库,如D3.js,创建一个简单的可视化图表:
- 创建一个div元素作为D3.js的容器
- 使用模板引用获取该div元素
- 在onMounted钩子中创建D3.js图表
- 在onUnmounted钩子中清理D3.js资源
10. 总结
模板引用是Vue 3中用于直接访问DOM元素或组件实例的强大机制,它具有以下特点:
- 组合式API优先,使用灵活、强大
- 响应式支持,自动更新
- 类型安全,支持TypeScript类型推断和检查
- 支持多个引用和动态引用
- 可以集成第三方库,实现复杂的交互效果
掌握模板引用的使用方法和最佳实践,是编写可靠、高效Vue组件的关键。通过合理使用模板引用,可以在声明式编程的基础上,灵活地操作DOM元素和组件实例,实现复杂的功能和交互效果。
11. 扩展阅读
通过本集的学习,我们深入理解了Vue 3中模板引用与$refs的使用方法和最佳实践。在下一集中,我们将学习自定义指令开发,这是Vue中用于扩展HTML元素功能的重要机制。