本文 AI 產出,尚未審核

Vue3 課程 – 響應式系統(Reactivity System)

主題:ref 與 DOM 互動


簡介

在 Vue 3 中,ref 是最基礎的響應式 API,負責將普通的 JavaScript 值包裝成「可追蹤」的資料。當 ref 的值改變時,所有依賴它的 templatecomputedwatch 都會自動重新計算,進而更新畫面。

然而,許多新手在把 ref 用於 DOM 互動時,會碰到「值已變但畫面沒更新」或「取得的 DOM 為 null」等問題。了解 ref 與實際 DOM 元素的映射方式,才能寫出既簡潔又可靠的互動程式碼。

本篇文章將從 概念實作範例常見陷阱,到 最佳實踐真實案例,一步步帶你掌握 ref 在 Vue 3 中與 DOM 互動的正確姿勢。


核心概念

1. ref 的兩種角色

角色 目的 使用情境
資料 ref 包裝原始資料(numberstringobject 等) 表單輸入、計數器、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. refv-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>

說明:此範例同時使用 資料 refemailemailError)與 DOM refemailInputemailErrorMsg),示範如何在驗證失敗時自動聚焦錯誤訊息,提高可存取性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記 .value setup() 中直接使用 ref 變數(如 count = 0)會失去響應性。 必須 透過 refVariable.value 讀寫。
setup 立即存取 DOM ref ref 在掛載前仍是 null,導致 TypeError 使用 onMountednextTickwatch 等生命週期鉤子。
同名衝突 template 中的 ref 名稱與 setup() 內的資料 ref 同名,會被模板覆寫。 若需要同時保存資料與 DOM,使用不同名稱(如 countRefcountEl)。
子組件未 expose 父層透過 ref 取得子組件實例卻無法呼叫方法。 在子組件 setup 中使用 expose({ method })
大量 ref 造成記憶體洩漏 未在 onBeforeUnmount 釋放第三方插件或事件監聽。 確保在組件卸載時 destroyremoveEventListener

最佳實踐

  1. 命名規則

    • 資料 refxxxRef(如 countRef
    • DOM refxxxEl(如 inputEl
  2. 聚焦與可存取性

    • 使用 tabindex="-1" 讓錯誤訊息或非可聚焦元素可被程式聚焦。
    • 在聚焦前先檢查 ref.value 是否存在。
  3. 避免過度使用 ref

    • 若只有單純的物件屬性變更,考慮使用 reactive
    • ref 適合 原始值單一物件DOM
  4. watch 配合

    • 想在 ref 變更時執行副作用(如 API 請求、DOM 操作)時,使用 watchwatchEffect

實際應用場景

場景 為什麼需要 ref + DOM 實作要點
自訂彈窗 / Modal 需要在開啟時自動聚焦第一個輸入框,關閉時回到觸發元素。 使用 modalEl 取得彈窗容器,triggerEl 取得觸發按鈕,配合 onMounted / onBeforeUnmount 控制焦點。
無限滾動 / 監測可視區 需要取得滾動容器的 scrollTopclientHeight 以判斷是否觸發載入。 scrollContainer 為 DOM ref,在 scroll 事件中讀取 scrollContainer.value.scrollTop
圖表、地圖、3D 渲染 第三方套件只能接受真實的 DOM 元素作為掛載點。 onMounted 內建立圖表實例,並在 onBeforeUnmount 銷毀。
表單動態驗證 驗證失敗時需把焦點移至錯誤訊息或特定欄位。 透過 errorMsgElinputEl 取得 DOM,使用 .focus()
動畫與過渡 需要直接操作元素的 styleclassList 或使用 requestAnimationFrame animEl 為 DOM ref,在 watchEffect 中根據狀態變更添加/移除 class。

總結

  • ref 是 Vue 3 中最直觀的響應式 API,分為 資料 refDOM ref 兩大類。
  • 使用 ref 時必須透過 .value 讀寫,並在 掛載完成 後才操作 DOM 元素。
  • 透過 onMountedwatchEffectwatch 等生命週期與副作用 API,可把資料變化與 DOM 操作緊密結合。
  • 常見的陷阱包括忘記 .value、提前存取 DOM、子組件未 expose 等,遵守命名規則與釋放資源的最佳實踐,可讓程式碼更安全、可維護。

掌握了 ref 與 DOM 的互動方式,你就能在 Vue 3 中輕鬆實作 聚焦、尺寸測量、第三方插件掛載、表單驗證 等常見需求,為使用者提供流暢且具可存取性的介面體驗。祝開發順利!