本文 AI 產出,尚未審核

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

主題:ref 在 template 的自動解包(unwrapping)


簡介

在 Vue 3 中,ref 是建立 單一值(primitive 或 object)可響應資料的最基礎 API。它與 reactive 的差別在於:ref 會把值包在一個具有 .value 屬性的容器裡,而 reactive 則直接將物件轉成 Proxy。在 template 裡使用 ref 時,Vue 會自動把 .value「解包」,讓開發者可以直接寫 {{ count }}:class="isActive",而不必手動加 .value

這項自動解包的機制不僅減少樣板程式碼,也降低新手忘記加 .value 而產生的錯誤。了解它的運作原理、限制與最佳實踐,對於寫出乾淨、可維護的 Vue 3 程式碼至關重要。


核心概念

1. ref 的基本使用

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)          // 包裝成 ref,內部值為 0
    const message = ref('Hello') // 文字也可以是 ref

    function increment() {
      count.value++               // 必須使用 .value 讀寫
    }

    return { count, message, increment }
  }
}

setup() 中返回的 ref 會在 template 中被自動解包,所以在下面的模板裡不需要寫 count.value

2. 為什麼在 template 會自動解包?

Vue 內部在編譯模板時,會把所有出現在 表達式{{ }}v-bindv-on 等)裡的變數,透過 proxyRefs 包裝一次。這個過程會檢查變數是否是 ref,若是則直接取出 .value,否則保持原樣。簡化的實作概念如下:

import { isRef, unref } from 'vue'

function proxyRefs(object) {
  return new Proxy(object, {
    get(target, key) {
      const val = Reflect.get(target, key)
      return isRef(val) ? val.value : val   // ★ 自動解包
    },
    set(target, key, newVal) {
      const oldVal = target[key]
      if (isRef(oldVal) && !isRef(newVal)) {
        oldVal.value = newVal                // 仍以 .value 寫入
        return true
      }
      return Reflect.set(target, key, newVal)
    }
  })
}

重點:自動解包只在 template 中生效,在 JavaScript 中仍需手動使用 .value(或 unref())。

3. refreactive 的混用

import { ref, reactive } from 'vue'

export default {
  setup() {
    const state = reactive({   // 物件本身是 reactive
      name: 'Vue',
      age: ref(3)              // 內部的屬性仍是 ref
    })

    // 在 JavaScript 中仍要用 .value
    console.log(state.age.value) // 3

    return { state }
  }
}

在 template 中,我們可以直接寫 {{ state.age }},Vue 會把 state.age(一個 ref)自動解包為 3但如果把 state 整個傳回 proxyRefs(state),則所有屬性都會自動解包,這在大型元件中非常便利。

4. unrefproxyRefs 的實務用法

import { ref, unref, proxyRefs } from 'vue'

export default {
  setup() {
    const foo = ref('bar')
    const baz = 'qux'

    // 手動解包
    const value = unref(foo)   // 等同於 foo.value

    // 包裝整個返回物件,使所有 ref 自動解包
    return proxyRefs({ foo, baz })
  }
}

使用 proxyRefs 後,返回的 foo 在 template 中不需要 .value,而在 JavaScript 中仍可直接使用 foo(會自動返回 .value),但若要重新指派一個新 ref,仍需注意 Proxy 的 set 行為。

5. 範例彙總

以下提供 五個實用範例,說明在不同情境下 ref 的自動解包如何配合 Vue 3 的特性運作。

範例 1:最簡單的計數器

<template>
  <button @click="increment">點擊次數:{{ count }}</button>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
function increment() {
  count.value++          // 必須在 JS 中使用 .value
}
</script>

說明{{ count }} 會被自動解包為 count.value,所以 UI 直接顯示數字。

範例 2:表單雙向綁定(v-model)

<template>
  <input v-model="name" placeholder="輸入姓名">
  <p>你輸入的是:{{ name }}</p>
</template>

<script setup>
import { ref } from 'vue'

const name = ref('')   // 文字輸入框的值
</script>

v-model 內部會呼叫 proxyRefs 的 set 方法,當使用者輸入時,Vue 會自動把新值寫入 name.value

範例 3:混合 reactiveref 的物件

<template>
  <p>年齡:{{ user.age }}</p>
  <button @click="grow">再長一歲</button>
</template>

<script setup>
import { reactive, ref } from 'vue'

const user = reactive({
  name: 'Alice',
  age: ref(20)   // age 用 ref 包裝
})

function grow() {
  user.age.value++   // 必須使用 .value
}
</script>

在 template 中直接使用 user.age,Vue 會自動解包為 user.age.value

範例 4:使用 proxyRefs 返回整個物件

<template>
  <p>狀態:{{ state.message }}</p>
  <button @click="toggle">切換</button>
</template>

<script setup>
import { ref, reactive, proxyRefs } from 'vue'

const state = reactive({
  message: ref('開啟')
})

function toggle() {
  state.message.value = state.message.value === '開啟' ? '關閉' : '開啟'
}

// 直接返回 proxyRefs 包裝過的物件
export default {
  setup() {
    return proxyRefs({ state })
  }
}
</script>

此時 state.message 在 template 中已被自動解包,且在 JavaScript 中也能直接讀寫(state.message 會返回 .value)。

範例 5:在 computed 中使用 ref

<template>
  <p>原始值:{{ count }}</p>
  <p>雙倍值:{{ doubleCount }}</p>
  <button @click="count++">+1</button>
</template>

<script setup>
import { ref, computed } from 'vue'

const count = ref(5)

// computed 會自動追蹤 count 的 .value
const doubleCount = computed(() => count.value * 2)
</script>

雖然 computed 內仍需要 count.value,但在 template 中使用 doubleCount 時,會自動解包。


常見陷阱與最佳實踐

陷阱 說明 解決方案
在 JavaScript 中忘記 .value 只在 template 有自動解包,程式碼裡仍需手動讀寫。 使用 unref()proxyRefs() 包裝返回物件,減少錯誤。
ref 包裝的陣列或物件仍是 shallow ref([]) 只會讓陣列本身是響應的,內部元素若是物件需要自行 reactive 需要時使用 ref + reactiveshallowRef
watch 中直接比較 ref 直接傳入 ref 會自動解包,但 deep 監聽時需注意。 使用 watch(() => myRef.value, ...) 以明確意圖。
proxyRefs 會改寫 set 行為 為了自動解包,proxyRefs 在設定新值時會自動寫入 .value,但如果你真的想替換整個 ref,需要使用 ref(newVal) 再賦值。 確認是否真的要替換整個 ref,或直接修改 .value
v-for 中使用 ref 每次迭代都會產生新的 ref,若不小心會導致不必要的重新渲染。 盡量在父層建立陣列的 ref,子項目只讀取值。

最佳實踐

  1. setup() 返回值前使用 proxyRefs,讓所有 ref 在 template 中自動解包,減少 .value 的混用。
  2. 對於需要深層 reactive 的結構,使用 reactive 而非大量嵌套的 ref
  3. 在 TypeScript 中,使用 Ref<T> 型別,搭配 unref() 可提升型別推斷的正確性。
  4. 盡量把邏輯寫在 composable,讓 ref 的使用範圍保持在同一層,避免跨層級的 .value 讀寫混亂。

實際應用場景

  1. 表單驗證
    每個欄位的錯誤訊息可以用 ref(''),在 template 中直接 {{ errorMsg }},不需要額外的 .value,讓 UI 與驗證邏輯分離。

  2. 即時搜尋(debounce)
    搜尋關鍵字使用 ref(''),在 watch 中監聽 keyword(自動解包),搭配 setTimeout 實作防抖,UI 仍只寫 {{ keyword }}

  3. 動態樣式切換
    isActive = ref(false),在 <div :class="{ active: isActive }"> 中自動解包,讓樣式切換更直觀。

  4. 多語系切換
    currentLocale = ref('zh-TW'),在 <p>{{ $t('welcome') }}</p> 中使用,配合 vue-i18nlocale 屬性,切換時只改 currentLocale.value

  5. 圖表資料更新
    chartData = ref([]),在圖表元件的 props 中直接傳入 :data="chartData",圖表庫會自動收到更新的陣列(因為 ref 已被解包)。


總結

  • ref 是 Vue 3 建立單值響應資料的核心 API。
  • 模板自動解包 讓開發者在 HTML 中直接使用 ref,減少 .value 的噪音。
  • 自動解包僅在 template 生效,程式碼中仍須手動使用 .valueunref()
  • 透過 proxyRefs 可以一次性把返回的物件全部自動解包,提升開發效率。
  • 正確認識 陷阱(忘記 .value、淺層 vs 深層響應)與 最佳實踐(適度使用 reactive、在 composable 中集中管理 ref)是寫出高品質 Vue 3 應用的關鍵。

掌握了 ref 在 template 的自動解包機制後,你可以更專注於 業務邏輯使用者體驗,而不必為繁瑣的 .value 讀寫分心。祝你在 Vue 3 的開發旅程中,寫出更乾淨、更易維護的程式碼!