Vue3 課程 – 響應式系統(Reactivity System)
主題:ref 與 DOM 互動
簡介
在 Vue 3 中,ref 是最基礎的響應式 API,負責將普通的 JavaScript 值包裝成「可追蹤」的資料。當 ref 的值改變時,所有依賴它的 template、computed 或 watch 都會自動重新計算,進而更新畫面。
然而,許多新手在把 ref 用於 DOM 互動時,會碰到「值已變但畫面沒更新」或「取得的 DOM 為 null」等問題。了解 ref 與實際 DOM 元素的映射方式,才能寫出既簡潔又可靠的互動程式碼。
本篇文章將從 概念、實作範例、常見陷阱,到 最佳實踐 與 真實案例,一步步帶你掌握 ref 在 Vue 3 中與 DOM 互動的正確姿勢。
核心概念
1. ref 的兩種角色
| 角色 | 目的 | 使用情境 |
|---|---|---|
資料 ref |
包裝原始資料(number、string、object 等) |
表單輸入、計數器、API 回傳結果 |
DOM ref |
取得模板中的實體 DOM 元素或子組件實例 | 手動聚焦、測量尺寸、第三方套件掛載點 |
重點:Vue 會自動在模板中把
ref名稱對應到setup()中同名的變數,無需額外的this.$refs。
2. 建立資料 ref
import { ref } from 'vue'
export default {
setup() {
const count = ref(0) // 包裝數字
const message = ref('Hello') // 包裝字串
function inc() {
count.value++ // 必須透過 .value 存取
}
return { count, message, inc }
}
}
ref 只是一個帶有 .value 屬性的容器,Vue 會在讀取或寫入 .value 時進行依賴追蹤。
3. 建立 DOM ref
<template>
<input type="text" ref="nameInput" @keyup.enter="focusNext" />
<button @click="focusInput">聚焦輸入框</button>
</template>
import { ref, onMounted } from 'vue'
export default {
setup() {
const nameInput = ref(null) // 初始為 null,等掛載完成後會被 Vue 填入
onMounted(() => {
// 此時 DOM 已經掛載,可直接使用
nameInput.value.focus()
})
function focusInput() {
if (nameInput.value) {
nameInput.value.focus()
}
}
function focusNext() {
// 這裡示範如何在同一個 component 中切換焦點
// 假設還有另一個 ref: lastNameInput
}
return { nameInput, focusInput, focusNext }
}
}
提示:在
setup()中宣告的ref會在onMounted之後自動指向真實的 DOM,若在mounted前就存取會得到null。
4. ref 與 v-model 的配合
<template>
<input v-model="search" placeholder="搜尋關鍵字" />
<button @click="clear">清除</button>
</template>
import { ref } from 'vue'
export default {
setup() {
const search = ref('') // 直接綁定於 v-model
function clear() {
search.value = '' // 變更會立即反映到 input
}
return { search, clear }
}
}
此例展示 v-model 內部其實是自動把 ref 的 .value 讀寫,開發者不需要額外寫 event.target.value。
5. 取得子組件實例(組件 ref)
<!-- Parent.vue -->
<template>
<ChildComponent ref="child" />
<button @click="callChildMethod">呼叫子組件方法</button>
</template>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
setup() {
const child = ref(null)
function callChildMethod() {
// 子組件必須 expose 方法才能被呼叫
child.value?.doSomething()
}
return { child, callChildMethod }
}
}
在子組件內:
export default {
setup(_, { expose }) {
function doSomething() {
console.log('子組件方法被呼叫')
}
expose({ doSomething }) // 必須使用 expose 讓父層可存取
}
}
程式碼範例
範例 1:自動聚焦並偵測尺寸變化
<template>
<div ref="box" class="box" @click="toggleSize">
點我改變大小
</div>
</template>
<script setup>
import { ref, onMounted, watchEffect } from 'vue'
const box = ref(null)
const large = ref(false)
function toggleSize() {
large.value = !large.value
}
// 監測元素尺寸,尺寸變化時印出寬高
watchEffect(() => {
if (box.value) {
const { offsetWidth, offsetHeight } = box.value
console.log(`寬度: ${offsetWidth}px, 高度: ${offsetHeight}px`)
}
})
onMounted(() => {
// 初始聚焦
box.value?.focus()
})
</script>
<style scoped>
.box {
width: 120px;
height: 120px;
background: #42b983;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.box.large {
width: 240px;
height: 240px;
}
</style>
說明:box 為 DOM ref,在 watchEffect 中直接使用 box.value 取得尺寸;large 為資料 ref,切換時會觸發 CSS class 變化。
範例 2:使用 ref 操作第三方插件(如 Chart.js)
<template>
<canvas ref="chartCanvas" width="400" height="200"></canvas>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { Chart, registerables } from 'chart.js'
Chart.register(...registerables)
const chartCanvas = ref(null)
let chartInstance = null
onMounted(() => {
if (chartCanvas.value) {
chartInstance = new Chart(chartCanvas.value, {
type: 'bar',
data: {
labels: ['A', 'B', 'C'],
datasets: [{ label: '數量', data: [12, 19, 3] }]
}
})
}
})
onBeforeUnmount(() => {
chartInstance?.destroy()
})
</script>
說明:第三方套件需要真實的 <canvas> 元素,透過 chartCanvas 取得後即可 instantiate。記得在元件卸載前釋放資源。
範例 3:表單驗證 – 取得錯誤訊息的 DOM 節點
<template>
<form @submit.prevent="submit">
<div>
<input v-model="email" ref="emailInput" placeholder="Email" />
<p v-if="emailError" class="error" ref="emailErrorMsg">{{ emailError }}</p>
</div>
<button type="submit">送出</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const email = ref('')
const emailError = ref('')
const emailInput = ref(null)
const emailErrorMsg = ref(null)
function validate() {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!pattern.test(email.value)) {
emailError.value = '請輸入有效的 Email'
// 把焦點跳到錯誤訊息上,讓螢幕閱讀器能即時讀出
emailErrorMsg.value?.focus()
return false
}
emailError.value = ''
return true
}
function submit() {
if (validate()) {
alert('表單送出成功!')
// 清空表單
email.value = ''
emailInput.value?.focus()
}
}
</script>
<style scoped>
.error {
color: red;
margin-top: 4px;
}
</style>
說明:此範例同時使用 資料 ref(email、emailError)與 DOM ref(emailInput、emailErrorMsg),示範如何在驗證失敗時自動聚焦錯誤訊息,提高可存取性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記 .value |
在 setup() 中直接使用 ref 變數(如 count = 0)會失去響應性。 |
必須 透過 refVariable.value 讀寫。 |
在 setup 立即存取 DOM ref |
ref 在掛載前仍是 null,導致 TypeError。 |
使用 onMounted、nextTick 或 watch 等生命週期鉤子。 |
| 同名衝突 | template 中的 ref 名稱與 setup() 內的資料 ref 同名,會被模板覆寫。 |
若需要同時保存資料與 DOM,使用不同名稱(如 countRef、countEl)。 |
子組件未 expose |
父層透過 ref 取得子組件實例卻無法呼叫方法。 |
在子組件 setup 中使用 expose({ method })。 |
大量 ref 造成記憶體洩漏 |
未在 onBeforeUnmount 釋放第三方插件或事件監聽。 |
確保在組件卸載時 destroy、removeEventListener。 |
最佳實踐
命名規則:
- 資料
ref→xxxRef(如countRef) - DOM
ref→xxxEl(如inputEl)
- 資料
聚焦與可存取性:
- 使用
tabindex="-1"讓錯誤訊息或非可聚焦元素可被程式聚焦。 - 在聚焦前先檢查
ref.value是否存在。
- 使用
避免過度使用
ref:- 若只有單純的物件屬性變更,考慮使用
reactive。 ref適合 原始值、單一物件 或 DOM。
- 若只有單純的物件屬性變更,考慮使用
與
watch配合:- 想在
ref變更時執行副作用(如 API 請求、DOM 操作)時,使用watch或watchEffect。
- 想在
實際應用場景
| 場景 | 為什麼需要 ref + DOM |
實作要點 |
|---|---|---|
| 自訂彈窗 / Modal | 需要在開啟時自動聚焦第一個輸入框,關閉時回到觸發元素。 | 使用 modalEl 取得彈窗容器,triggerEl 取得觸發按鈕,配合 onMounted / onBeforeUnmount 控制焦點。 |
| 無限滾動 / 監測可視區 | 需要取得滾動容器的 scrollTop、clientHeight 以判斷是否觸發載入。 |
scrollContainer 為 DOM ref,在 scroll 事件中讀取 scrollContainer.value.scrollTop。 |
| 圖表、地圖、3D 渲染 | 第三方套件只能接受真實的 DOM 元素作為掛載點。 | 在 onMounted 內建立圖表實例,並在 onBeforeUnmount 銷毀。 |
| 表單動態驗證 | 驗證失敗時需把焦點移至錯誤訊息或特定欄位。 | 透過 errorMsgEl、inputEl 取得 DOM,使用 .focus()。 |
| 動畫與過渡 | 需要直接操作元素的 style、classList 或使用 requestAnimationFrame。 |
animEl 為 DOM ref,在 watchEffect 中根據狀態變更添加/移除 class。 |
總結
ref是 Vue 3 中最直觀的響應式 API,分為 資料ref與 DOMref兩大類。- 使用
ref時必須透過.value讀寫,並在 掛載完成 後才操作 DOM 元素。 - 透過
onMounted、watchEffect、watch等生命週期與副作用 API,可把資料變化與 DOM 操作緊密結合。 - 常見的陷阱包括忘記
.value、提前存取 DOM、子組件未expose等,遵守命名規則與釋放資源的最佳實踐,可讓程式碼更安全、可維護。
掌握了 ref 與 DOM 的互動方式,你就能在 Vue 3 中輕鬆實作 聚焦、尺寸測量、第三方插件掛載、表單驗證 等常見需求,為使用者提供流暢且具可存取性的介面體驗。祝開發順利!