Skip to content

Template Refs:存取 DOM 與元件實體

重新認識 Vue.js: 008 天絕對看不完的 Vue.js 3.x 指南

宣告式渲染讓我們幾乎不用碰 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> 執行期間是 nullonMounted 之後才拿得到元素。

常見錯誤:在掛載完成前讀 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,元件之間的溝通管道到齊了。接下來換一個主題:同一個位置要輪流顯示不同元件時,怎麼切換、怎麼保留狀態。