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><