Vue 3.5 重磅新特性:useTemplateRef 让模板引用更优雅、更高效!
Vue 3.5 重磅新特性:useTemplateRef 让模板引用更优雅、更高效!
目录
- 前言
- 什么是 useTemplateRef
- 传统 ref 的问题
- useTemplateRef 的优势
- 基础用法
- 进阶用法
- 最佳实践
- 迁移指南
- 性能对比
- 注意事项
- 总结
前言
Vue 3.5 带来了一个激动人心的新特性 useTemplateRef
,它彻底革新了我们在 Vue 3 中处理模板引用的方式。这个新的 Composition API 不仅让代码更加优雅,还提供了更好的类型安全性和性能优化。
什么是 useTemplateRef
useTemplateRef
是 Vue 3.5 新增的 Composition API,专门用于处理模板引用(template refs)。它提供了一种更直观、更类型安全的方式来访问 DOM 元素和组件实例。
核心特性
- 🎯 类型安全:完美的 TypeScript 支持
- 🚀 性能优化:更高效的内部实现
- 💡 简洁语法:更直观的 API 设计
- 🔄 响应式:与 Vue 的响应式系统深度集成
传统 ref 的问题
在 Vue 3.5 之前,我们通常这样处理模板引用:
问题示例
<template><div><input ref="inputRef" /><button @click="focusInput">聚焦输入框</button><MyComponent ref="componentRef" /></div>
</template><script setup lang="ts">
import { ref, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'// 问题1:类型推断不够精确
const inputRef = ref<HTMLInputElement>()
const componentRef = ref<InstanceType<typeof MyComponent>>()// 问题2:需要手动类型断言
const focusInput = () => {inputRef.value?.focus() // 需要可选链
}// 问题3:在 onMounted 之前 ref.value 为 undefined
onMounted(() => {console.log(inputRef.value) // 可能为 undefined
})
</script>
存在的问题
- 类型推断复杂:需要手动指定泛型类型
- 运行时检查:需要使用可选链操作符
- 生命周期依赖:只能在特定生命周期后使用
- 代码冗余:重复的类型声明和空值检查
useTemplateRef 的优势
1. 类型安全
<script setup lang="ts">
import { useTemplateRef } from 'vue'// 自动类型推断,无需手动指定类型
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')// TypeScript 会自动推断出正确的类型
const focusInput = () => {inputRef.value?.focus() // 完美的类型提示
}
</script>
2. 更好的性能
// 内部优化,减少不必要的响应式开销
const elementRef = useTemplateRef('elementRef')// 自动优化,只在需要时创建响应式引用
3. 简洁的 API
<template><input ref="inputRef" /><button ref="buttonRef" @click="handleClick">点击</button>
</template><script setup lang="ts">
import { useTemplateRef } from 'vue'// 一行代码搞定
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')const handleClick = () => {inputRef.value?.focus()
}
</script>
基础用法
1. DOM 元素引用
<template><div><input ref="usernameInput" placeholder="请输入用户名"@keyup.enter="handleSubmit"/><button ref="submitButton" @click="handleSubmit">提交</button><div ref="messageContainer"></div></div>
</template><script setup lang="ts">
import { useTemplateRef, nextTick } from 'vue'// 创建模板引用
const usernameInput = useTemplateRef<HTMLInputElement>('usernameInput')
const submitButton = useTemplateRef<HTMLButtonElement>('submitButton')
const messageContainer = useTemplateRef<HTMLDivElement>('messageContainer')// 聚焦输入框
const focusUsername = () => {usernameInput.value?.focus()
}// 提交处理
const handleSubmit = async () => {const username = usernameInput.value?.valueif (!username) {await showMessage('请输入用户名', 'error')focusUsername()return}// 禁用按钮if (submitButton.value) {submitButton.value.disabled = true}try {// 模拟 API 调用await submitForm(username)await showMessage('提交成功!', 'success')} catch (error) {await showMessage('提交失败,请重试', 'error')} finally {// 恢复按钮状态if (submitButton.value) {submitButton.value.disabled = false}}
}// 显示消息
const showMessage = async (text: string, type: 'success' | 'error') => {if (!messageContainer.value) returnmessageContainer.value.textContent = textmessageContainer.value.className = `message ${type}`await nextTick()// 3秒后清除消息setTimeout(() => {if (messageContainer.value) {messageContainer.value.textContent = ''messageContainer.value.className = ''}}, 3000)
}// 模拟 API 调用
const submitForm = (username: string): Promise<void> => {return new Promise((resolve, reject) => {setTimeout(() => {Math.random() > 0.3 ? resolve() : reject(new Error('网络错误'))}, 1000)})
}
</script><style scoped>
.message {padding: 8px;margin-top: 10px;border-radius: 4px;
}.message.success {background-color: #d4edda;color: #155724;border: 1px solid #c3e6cb;
}.message.error {background-color: #f8d7da;color: #721c24;border: 1px solid #f5c6cb;
}
</style>
2. 组件实例引用
<template><div><UserProfile ref="userProfileRef" :user-id="currentUserId"/><AdminPanel ref="adminPanelRef" v-if="isAdmin"/><button @click="refreshUserData">刷新用户数据</button><button @click="openAdminSettings" v-if="isAdmin">管理员设置</button></div>
</template><script setup lang="ts">
import { useTemplateRef, ref } from 'vue'
import UserProfile from './components/UserProfile.vue'
import AdminPanel from './components/AdminPanel.vue'// 组件引用
const userProfileRef = useTemplateRef<InstanceType<typeof UserProfile>>('userProfileRef')
const adminPanelRef = useTemplateRef<InstanceType<typeof AdminPanel>>('adminPanelRef')// 数据
const currentUserId = ref(123)
const isAdmin = ref(true)// 刷新用户数据
const refreshUserData = async () => {// 调用子组件的方法await userProfileRef.value?.refreshData()// 获取子组件的数据const userData = userProfileRef.value?.getUserData()console.log('用户数据:', userData)
}// 打开管理员设置
const openAdminSettings = () => {// 调用管理员面板的方法adminPanelRef.value?.openSettings()// 访问管理员面板的状态const isSettingsOpen = adminPanelRef.value?.settingsVisibleconsole.log('设置面板状态:', isSettingsOpen)
}
</script>
3. 动态引用
<template><div><div v-for="(item, index) in items" :key="item.id":ref="el => setItemRef(el, index)"class="item">{{ item.name }}</div><button @click="highlightRandomItem">随机高亮</button></div>
</template><script setup lang="ts">
import { ref, onUpdated } from 'vue'interface Item {id: numbername: string
}const items = ref<Item[]>([{ id: 1, name: '项目 1' },{ id: 2, name: '项目 2' },{ id: 3, name: '项目 3' },{ id: 4, name: '项目 4' },{ id: 5, name: '项目 5' }
])// 动态引用集合
const itemRefs = ref<HTMLDivElement[]>([])// 设置动态引用
const setItemRef = (el: Element | null, index: number) => {if (el && el instanceof HTMLDivElement) {itemRefs.value[index] = el}
}// 清理无效引用
onUpdated(() => {itemRefs.value = itemRefs.value.slice(0, items.value.length)
})// 高亮随机项目
const highlightRandomItem = () => {// 清除之前的高亮itemRefs.value.forEach(el => {if (el) {el.classList.remove('highlight')}})// 随机选择一个项目高亮const randomIndex = Math.floor(Math.random() * items.value.length)const targetElement = itemRefs.value[randomIndex]if (targetElement) {targetElement.classList.add('highlight')targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' })}
}
</script><