Appearance
Template Refs:存取 DOM 與元件實體
宣告式渲染讓我們幾乎不用碰 DOM,但「幾乎」不是「完全」:對焦輸入框、量測元素尺寸、控制影片播放,這些操作沒有對應的模板語法,就是得拿到 DOM 元素本身。上一章講掛載時序時預告過的正式管道,就是 template ref。
useTemplateRef() (Vue 3.5 新增)
兩步驟:模板上用 ref 屬性替元素取名,script 裡用 useTemplateRef() 取得它:
src/App.vue:
vue
<script setup>
import { onMounted, useTemplateRef } from 'vue'
const searchInput = useTemplateRef('search-input')
onMounted(() => {
searchInput.value.focus() // 掛載完成後,自動對焦搜尋框
})
</script>
<template>
<main class="app">
<input ref="search-input" type="search" placeholder="搜尋書名">
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>useTemplateRef() 回傳一個 ref,.value 就是那個 DOM 元素,可以呼叫任何原生方法。頁面一載入,游標自動落在搜尋框裡。
讀舊程式碼會看到另一種寫法
useTemplateRef() 是 Vue 3.5 才有的。在那之前的寫法,是宣告一個與模板 ref 屬性同名的 ref(null),Vue 會自動對應。兩者效果相同,新專案建議用 useTemplateRef():對應關係是明確的參數,不再依賴「剛好同名」的默契。
ref 什麼時候有值
上一章鋪的時序觀念在這裡兌現:元件自己的模板 DOM 要到掛載後才存在,所以 template ref 在 <script setup> 執行期間是 null,onMounted 之後才拿得到元素。
常見錯誤:在掛載完成前讀 template ref
js
const searchInput = useTemplateRef('search-input')
searchInput.value.focus() // 錯誤:setup 執行中,DOM 還不存在,這裡是 null
onMounted(() => {
searchInput.value.focus() // 正確:掛載完成,元素就位
})還有一個容易忽略的情況:ref 不是「一旦有值就永遠有值」。如果元素掛在 v-if 底下,條件變 false、元素卸載後,ref 會回到 null。在事件處理或 watch 裡使用 template ref 時,記得防範:searchInput.value?.focus()。
搭配 nextTick():等 DOM 更新完再動手
上一章講響應式原理時提過:改了資料,Vue 是排程批次更新,不會當場重畫 DOM。這對 template ref 的操作有直接影響:改完資料立刻去量 DOM,量到的是舊畫面。await nextTick() 就是為此存在的等待點。
實際場景:訊息清單新增一筆後,自動捲到最底部:
src/App.vue:
vue
<script setup>
import { nextTick, ref, useTemplateRef } from 'vue'
const messages = ref(['歡迎光臨 Kuro 書店'])
const listEl = useTemplateRef('message-list')
async function addMessage() {
messages.value.push(`第 ${messages.value.length + 1} 則訊息`)
await nextTick() // 等 Vue 把新的 <li> 真正放進 DOM
// 這時 scrollHeight 才包含新訊息的高度,捲動才會到底
listEl.value.scrollTop = listEl.value.scrollHeight
}
</script>
<template>
<main class="app">
<ul ref="message-list" class="messages">
<li v-for="message in messages" :key="message">{{ message }}</li>
</ul>
<button type="button" @click="addMessage">
新增訊息
</button>
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
.messages {
height: 8rem;
overflow-y: auto;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 0.5rem 0.5rem 0.5rem 1.75rem;
}
</style>把 await nextTick() 那行拿掉試試:捲動永遠慢一步,停在倒數第二則訊息的位置。加回來,改資料、等更新、量測 DOM 三步到位。這就是上一章預告過的 nextTick() 完整用法。
元件 refs 與 defineExpose
ref 屬性放在子元件標籤上,拿到的不是 DOM 元素,而是元件實體。不過先講原則:父子溝通優先用 props 與 emits,元件 ref 是保留給「必要的 imperative API」的後門,例如要求子元件對焦、播放、重置這類指令式操作。
而且這扇後門預設是關的:<script setup> 的元件預設封閉,父元件透過 ref 什麼都拿不到。子元件要用 defineExpose() 明確列出願意開放的東西:
src/components/SearchBox.vue:
vue
<script setup>
import { ref, useTemplateRef } from 'vue'
const keyword = ref('')
const inputEl = useTemplateRef('input')
function focus() {
inputEl.value?.focus()
}
defineExpose({ focus }) // 只開放 focus;keyword 等內部狀態不外露
</script>
<template>
<input ref="input" v-model="keyword" type="search" placeholder="搜尋書名">
</template>src/App.vue:
vue
<script setup>
import { onMounted, useTemplateRef } from 'vue'
import SearchBox from './components/SearchBox.vue'
const searchBox = useTemplateRef('search-box')
onMounted(() => {
searchBox.value.focus() // 呼叫的是 SearchBox 開放出來的方法
})
</script>
<template>
<main class="app">
<SearchBox ref="search-box" />
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>SearchBox 把「對焦」包成自己的方法開放出去,內部的 keyword 則不外露。介面因此保持窄而明確:資料流走 props/emits,指令式操作走 expose 出來的方法,各行其道。
TypeScript 版本:template ref 的型別
Vue 3.5 搭配 @vue/language-tools 2.1 以上,可以自動推導 SFC 靜態 template ref 的型別,多數情況不必手動標註。動態元件這類無法推導的情況,再明確指定泛型:
ts
const searchInput = useTemplateRef<HTMLInputElement>('search-input')同樣地,型別的完整討論留給第九章。
props、emits、slots、provide/inject、template refs,元件之間的溝通管道到齊了。接下來換一個主題:同一個位置要輪流顯示不同元件時,怎麼切換、怎麼保留狀態。
