Vue Template Refs
在 Vue 中,雖然大多數情況下我們透過資料驅動的方式來操作 DOM,但有時候還是需要直接存取 DOM 元素。Template Refs(模板引用)提供了一種方式,讓你可以在元件中取得 DOM 元素或子元件的實例。
基本用法
使用 ref 屬性來標記元素,然後在 <script setup> 中用同名的 ref 變數來存取:
<script setup>
import { ref, onMounted } from 'vue'
// 宣告一個 ref 來存放元素引用
const inputRef = ref(null)
onMounted(() => {
// 元件掛載後,inputRef.value 會是 DOM 元素
inputRef.value.focus()
})
</script>
<template>
<!-- ref 屬性的值對應到變數名稱 -->
<input ref="inputRef" type="text">
</template>
只有在元件掛載後(
onMounted),ref 才會被賦值。在 onMounted 之前,ref 的值是 null。存取子元件
ref 也可以用來存取子元件的實例:
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
onMounted(() => {
// 存取子元件的方法或屬性
childRef.value.someMethod()
})
</script>
<template>
<ChildComponent ref="childRef" />
</template>
子元件需要 expose
在 <script setup> 中,元件預設是「封閉」的,父元件無法存取內部的屬性和方法。需要使用 defineExpose 明確暴露:
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function reset() {
count.value = 0
}
// 暴露給父元件
defineExpose({
count,
increment,
reset
})
</script>
更多 defineExpose 的用法請參考 defineExpose。
v-for 中的 Refs
當 ref 用在 v-for 中時,ref 會是一個陣列:
<script setup>
import { ref, onMounted } from 'vue'
const items = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
// itemRefs.value 是一個 DOM 元素陣列
console.log(itemRefs.value)
})
</script>
<template>
<ul>
<li v-for="item in items" :key="item" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
ref 陣列的順序不保證與原始陣列一致。
函式 Refs
ref 屬性也可以綁定一個函式,每次元件更新時都會呼叫:
<script setup>
import { ref } from 'vue'
const inputEl = ref(null)
function setInputRef(el) {
// el 是 DOM 元素,或在卸載時是 null
inputEl.value = el
}
</script>
<template>
<input :ref="setInputRef">
</template>
這在需要更複雜的 ref 邏輯時很有用,例如根據條件選擇性地儲存 ref。
常見使用情境
聚焦輸入框
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
// 也可以在方法中使用
function focusInput() {
inputRef.value?.focus()
}
</script>
<template>
<input ref="inputRef" type="text">
<button @click="focusInput">聚焦</button>
</template>
取得元素尺寸
<script setup>
import { ref, onMounted } from 'vue'
const boxRef = ref(null)
const dimensions = ref({ width: 0, height: 0 })
onMounted(() => {
const rect = boxRef.value.getBoundingClientRect()
dimensions.value = {
width: rect.width,
height: rect.height
}
})
</script>
<template>
<div ref="boxRef" class="box">
尺寸:{{ dimensions.width }} x {{ dimensions.height }}
</div>
</template>
滾動到元素
<script setup>
import { ref } from 'vue'
const sectionRef = ref(null)
function scrollToSection() {
sectionRef.value?.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
</script>
<template>
<button @click="scrollToSection">滾動到區塊</button>
<div style="height: 1000px;">長內容...</div>
<section ref="sectionRef">
<h2>目標區塊</h2>
</section>
</template>
整合第三方函式庫
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
const canvasRef = ref(null)
let chartInstance = null
const props = defineProps({
data: Array
})
onMounted(() => {
// 初始化圖表
chartInstance = new Chart(canvasRef.value, {
type: 'bar',
data: {
datasets: [{ data: props.data }]
}
})
})
// 資料更新時更新圖表
watch(() => props.data, (newData) => {
if (chartInstance) {
chartInstance.data.datasets[0].data = newData
chartInstance.update()
}
})
onUnmounted(() => {
// 清理
if (chartInstance) {
chartInstance.destroy()
}
})
</script>
<template>
<canvas ref="canvasRef"></canvas>
</template>
表單驗證
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
const emailRef = ref(null)
function validateForm() {
// 使用原生 HTML5 驗證
if (!formRef.value.checkValidity()) {
formRef.value.reportValidity()
return false
}
return true
}
function focusEmail() {
emailRef.value?.focus()
}
</script>
<template>
<form ref="formRef" @submit.prevent="validateForm">
<input ref="emailRef" type="email" required placeholder="Email">
<input type="password" required minlength="6" placeholder="密碼">
<button type="submit">送出</button>
</form>
</template>
動態取得多個 refs
<script setup>
import { ref, onMounted } from 'vue'
const items = ref(['A', 'B', 'C', 'D'])
const itemRefs = ref(new Map())
function setItemRef(el, item) {
if (el) {
itemRefs.value.set(item, el)
} else {
itemRefs.value.delete(item)
}
}
function scrollToItem(item) {
const el = itemRefs.value.get(item)
el?.scrollIntoView({ behavior: 'smooth' })
}
onMounted(() => {
console.log('所有 refs:', itemRefs.value)
})
</script>
<template>
<nav>
<button v-for="item in items" :key="item" @click="scrollToItem(item)">
跳到 {{ item }}
</button>
</nav>
<div
v-for="item in items"
:key="item"
:ref="(el) => setItemRef(el, item)"
class="section"
>
區塊 {{ item }}
</div>
</template>
呼叫子元件方法
<!-- CounterChild.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
defineExpose({
count,
increment,
decrement,
reset
})
</script>
<template>
<div class="counter">
計數:{{ count }}
</div>
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import CounterChild from './CounterChild.vue'
const counterRef = ref(null)
function handleIncrement() {
counterRef.value?.increment()
}
function handleReset() {
counterRef.value?.reset()
}
function getCount() {
return counterRef.value?.count
}
</script>
<template>
<CounterChild ref="counterRef" />
<button @click="handleIncrement">+1</button>
<button @click="handleReset">重置</button>
<button @click="() => console.log(getCount())">取得計數</button>
</template>
搭配 TypeScript
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM 元素
const inputRef = ref<HTMLInputElement | null>(null)
// 子元件(假設有型別定義)
import ChildComponent from './ChildComponent.vue'
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)
onMounted(() => {
inputRef.value?.focus()
childRef.value?.someMethod()
})
</script>
<template>
<input ref="inputRef" type="text">
<ChildComponent ref="childRef" />
</template>
最佳實踐
優先使用資料驅動:大多數情況下,應該透過響應式資料來控制 DOM,而不是直接操作
在 onMounted 後存取:確保 DOM 已經存在
使用可選鏈:
ref.value?.method()避免 null 錯誤子元件使用 defineExpose:明確暴露需要被存取的內容
清理第三方函式庫:在
onUnmounted中銷毀實例避免過度使用:如果發現大量使用 refs 來操作 DOM,可能需要重新思考設計