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-bind、v-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. ref 與 reactive 的混用
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. unref 與 proxyRefs 的實務用法
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:混合 reactive 與 ref 的物件
<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 + reactive 或 shallowRef。 |
在 watch 中直接比較 ref |
直接傳入 ref 會自動解包,但 deep 監聽時需注意。 |
使用 watch(() => myRef.value, ...) 以明確意圖。 |
proxyRefs 會改寫 set 行為 |
為了自動解包,proxyRefs 在設定新值時會自動寫入 .value,但如果你真的想替換整個 ref,需要使用 ref(newVal) 再賦值。 |
確認是否真的要替換整個 ref,或直接修改 .value。 |
在 v-for 中使用 ref |
每次迭代都會產生新的 ref,若不小心會導致不必要的重新渲染。 |
盡量在父層建立陣列的 ref,子項目只讀取值。 |
最佳實踐:
- 在
setup()返回值前使用proxyRefs,讓所有ref在 template 中自動解包,減少.value的混用。 - 對於需要深層 reactive 的結構,使用
reactive而非大量嵌套的ref。 - 在 TypeScript 中,使用
Ref<T>型別,搭配unref()可提升型別推斷的正確性。 - 盡量把邏輯寫在 composable,讓
ref的使用範圍保持在同一層,避免跨層級的.value讀寫混亂。
實際應用場景
表單驗證
每個欄位的錯誤訊息可以用ref(''),在 template 中直接{{ errorMsg }},不需要額外的.value,讓 UI 與驗證邏輯分離。即時搜尋(debounce)
搜尋關鍵字使用ref(''),在watch中監聽keyword(自動解包),搭配setTimeout實作防抖,UI 仍只寫{{ keyword }}。動態樣式切換
isActive = ref(false),在<div :class="{ active: isActive }">中自動解包,讓樣式切換更直觀。多語系切換
currentLocale = ref('zh-TW'),在<p>{{ $t('welcome') }}</p>中使用,配合vue-i18n的locale屬性,切換時只改currentLocale.value。圖表資料更新
chartData = ref([]),在圖表元件的props中直接傳入:data="chartData",圖表庫會自動收到更新的陣列(因為 ref 已被解包)。
總結
ref是 Vue 3 建立單值響應資料的核心 API。- 模板自動解包 讓開發者在 HTML 中直接使用
ref,減少.value的噪音。 - 自動解包僅在 template 生效,程式碼中仍須手動使用
.value或unref()。 - 透過
proxyRefs可以一次性把返回的物件全部自動解包,提升開發效率。 - 正確認識 陷阱(忘記
.value、淺層 vs 深層響應)與 最佳實踐(適度使用reactive、在 composable 中集中管理ref)是寫出高品質 Vue 3 應用的關鍵。
掌握了 ref 在 template 的自動解包機制後,你可以更專注於 業務邏輯 與 使用者體驗,而不必為繁瑣的 .value 讀寫分心。祝你在 Vue 3 的開發旅程中,寫出更乾淨、更易維護的程式碼!