Vue3 Composition API 核心 – ref() 與 reactive()
簡介
在 Vue 3 中,Composition API 取代了 Options API 成為官方推薦的寫法,讓我們可以以函式的方式組織邏輯,提升程式碼的可讀性與可重用性。
在所有 Composition API 的工具裡,ref() 與 reactive() 是最基礎、也是最常使用的兩個 API——它們負責 建立響應式資料,讓 Vue 能自動追蹤變化並重新渲染 UI。
本篇文章將深入探討 ref() 與 reactive() 的差異、使用時機、實作細節以及常見的坑,幫助 初學者 能快速上手,同時提供 中階開發者 在大型專案中維護響應式狀態的最佳實踐。
核心概念
1. ref() – 包裝原始值或單一物件
ref() 接收 任意類型(原始值、物件、陣列、函式…),回傳一個 包含 .value 屬性的 Ref 物件。Vue 會在 .value 被讀取或寫入時自動追蹤依賴,並在變更時觸發更新。
import { ref } from 'vue'
// 包裝原始數值
const count = ref(0)
// 包裝字串
const message = ref('Hello Vue 3')
// 包裝陣列(仍然需要 .value 存取)
const items = ref([1, 2, 3])
為什麼要使用 .value?
- Vue 必須在 讀取 時建立依賴(getter)以及在 寫入 時觸發通知(setter)。
- 透過
.value,Vue 能在底層使用Object.defineProperty或 Proxy 來攔截存取,保持效能與正確性。
小技巧:在
<template>中直接使用ref,Vue 會自動解包,不需要寫.value。
<template>
<p>計數:{{ count }}</p> <!-- Vue 會自動把 count.value 取出 -->
<button @click="count++">+1</button>
</template>
2. reactive() – 深層代理整個物件
reactive() 只接受 物件(包括陣列)作為參數,回傳一個 深層 Proxy。這意味著物件內所有屬性(包含巢狀屬性)都會被自動轉成響應式,不需要 .value。
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 25,
address: {
city: 'Taipei',
zip: '100'
}
})
reactive() 與 ref() 的差異
| 特性 | ref() |
reactive() |
|---|---|---|
| 接受類型 | 任意(原始值、物件、陣列) | 只接受物件(含陣列) |
| 存取方式 | .value(在 JS 中) |
直接屬性存取 |
| 深層代理 | 只代理最外層(若包裝物件,內部屬性仍是普通值) | 自動深層代理全部屬性 |
在 <template> 中的表現 |
自動解包 | 直接使用 |
3. 何時選擇 ref(),何時選擇 reactive()
| 場景 | 建議使用 |
|---|---|
| 需要 單一值(數字、字串、布林)或 簡單陣列 | ref() |
| 要管理 較複雜的物件結構(多層巢狀、需要解構) | reactive() |
| 想要 保持原始物件的引用(例如傳遞給第三方函式) | ref()(將物件包在 ref 中) |
| 需要 在組件外共享狀態(如 Pinia、Vuex) | 兩者皆可,視需求而定;通常使用 reactive 包裝整個 store |
程式碼範例
以下示範 5 個實務中常見的使用方式,並在每段程式碼後加上說明。
範例 1:計數器(最簡單的 ref)
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<p>目前計數:{{ count }}</p>
<button @click="increment">+1</button>
</template>
說明:count 為 ref,在 <template> 中 Vue 會自動解包,increment 函式直接操作 .value。
範例 2:表單資料的雙向綁定(reactive)
<script setup>
import { reactive } from 'vue'
const form = reactive({
username: '',
email: '',
agree: false
})
</script>
<template>
<label>
使用者名稱:
<input v-model="form.username" />
</label>
<label>
Email:
<input v-model="form.email" type="email" />
</label>
<label>
<input v-model="form.agree" type="checkbox" />
同意條款
</label>
<pre>{{ form }}</pre>
</template>
說明:reactive 讓 form 內所有屬性自動成為響應式,v-model 直接綁定即可。
範例 3:混合使用 ref 與 reactive(物件包在 ref 中)
<script setup>
import { ref } from 'vue'
// 將物件包在 ref 中,保留原始引用
const settings = ref({
theme: 'light',
language: 'zh-TW'
})
// 更新方式
function toggleTheme() {
settings.value.theme = settings.value.theme === 'light' ? 'dark' : 'light'
}
</script>
<template>
<p>主題:{{ settings.theme }}</p>
<button @click="toggleTheme">切換主題</button>
</template>
說明:把整個設定物件包在 ref,可以在需要「保持同一個引用」的情況下使用(例如傳入第三方函式)。在模板中同樣會自動解包。
範例 4:陣列的增刪(ref 包裝陣列)
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: '學習 Vue 3', done: false },
{ id: 2, text: '寫教學文章', done: false }
])
function addTodo(text) {
const newTodo = { id: Date.now(), text, done: false }
todos.value.push(newTodo) // 直接操作 .value
}
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
<input v-model="newText" placeholder="新增待辦" />
<button @click="addTodo(newText)">加入</button>
</template>
說明:陣列本身是透過 ref 包裝,操作時仍須使用 .value,但在模板中會自動解包。
範例 5:深層監控與 toRefs(從 reactive 取出單獨的 Ref)
<script setup>
import { reactive, toRefs, watch } from 'vue'
const profile = reactive({
name: 'Bob',
address: {
city: 'Kaohsiung',
zip: '800'
}
})
// 把每個屬性轉成獨立的 ref,方便解構
const { name, address } = toRefs(profile)
// 監聽單一屬性變化
watch(name, (newVal, oldVal) => {
console.log(`姓名從 ${oldVal} 改成 ${newVal}`)
})
</script>
<template>
<p>姓名:{{ name }}</p>
<p>城市:{{ address.city }}</p>
<button @click="name = 'Alice'">改名</button>
</template>
說明:
toRefs()把reactive物件的每個屬性轉成ref,保留原始響應式連結。- 這樣在解構(
const { name } = profile)後仍能保持響應式,避免「失去追蹤」的常見錯誤。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記 .value |
在 JavaScript 中直接使用 ref 而未加 .value,導致更新不會觸發 UI。 |
養成習慣:只在 setup、watch、computed 等純 JS 區塊使用 .value,模板中則可直接使用。 |
| 解構會失去響應式 | const { foo } = reactiveObj 會產生普通變數,失去追蹤。 |
使用 toRefs() 或 toRef() 取得保留響應式的 Ref。 |
混用 ref 與 reactive 產生雙重代理 |
把已是 reactive 的物件再包在 ref,會產生不必要的 Proxy,影響效能。 |
只使用其中一個:若需要深層代理,直接 reactive;若只想保留引用,使用 ref 包裝。 |
| 陣列變更不觸發更新(舊版 Vue 2 的問題) | 在 Vue 3 中已不會發生,但仍需注意使用 push/pop 等變更陣列的方法時,仍要透過 .value。 |
確保對 ref 包裝的陣列使用 .value,或直接使用 reactive 陣列。 |
對同一資料同時使用 ref 與 reactive |
會導致兩套獨立的依賴圖,更新時可能不一致。 | 統一管理:在同一層級僅選擇 ref 或 reactive,或使用 shallowRef/shallowReactive 針對需求做區分。 |
最佳實踐
- 保持單一來源:在同一個模組或組件內,盡量只使用
ref或reactive,避免混雜造成維護困難。 - 使用
toRefs解構:在需要把reactive物件解構傳遞給子組件時,使用toRefs以保留響應式。 - 適度使用
shallowRef/shallowReactive:若只想讓外層變化可觀測,而內層保持普通物件,可使用這兩個「淺層」API,提升效能。 - 型別提示(TypeScript):
ref<number>(0)、reactive<User>({...})能讓編譯器協助捕捉錯誤。 - 保持一致的命名慣例:慣例上,
ref變數常以xxxRef或直接語意名稱(如count)命名;reactive變數則以描述性名稱(如form、profile)命名。
實際應用場景
| 場景 | 使用 API | 為什麼 |
|---|---|---|
| 表單驗證 | reactive + toRefs |
表單欄位多且需要深層監控,toRefs 讓子組件拿到單獨的 ref。 |
| 計時器 / 動畫狀態 | ref(單一數值或布林) |
數值頻繁變動,使用 ref 能避免不必要的深層代理。 |
| 全局設定(如主題、語系) | ref 包裝物件或 reactive store |
若要在多個模組間共享相同引用,ref 更直觀;若設定結構較複雜,reactive 方便深層變更。 |
| 第三方函式庫的配置 | ref 包裝整個配置物件 |
多數第三方函式接受普通 JS 物件,使用 ref 可保留引用不被 Vue 代理破壞。 |
| 列表渲染與局部更新 | ref 包裝陣列 |
陣列本身是單一值,使用 ref 可直接操作 .value,且在模板中自動解包。 |
| 複雜的樹狀結構(如樹狀選單) | reactive(深層代理) |
需要對巢狀節點進行頻繁增刪改,reactive 能自動追蹤所有層級。 |
總結
ref() 與 reactive() 是 Vue 3 Composition API 中 建立響應式資料的兩大根基。
ref()以 單一值 為主,透過.value讓 Vue 追蹤讀寫;在模板中會自動解包,使用上非常直觀。reactive()則提供 深層代理,適合管理複雜物件結構,直接以屬性存取即可。
掌握兩者的差異、正確的使用時機以及常見的坑,能讓你在開發 Vue 應用時寫出 更乾淨、可維護且效能友好 的程式碼。記得:
- 根據資料結構選擇 API(單值 →
ref、物件 →reactive)。 - 在 JavaScript 區塊內務必使用
.value,或使用toRefs/toRef保留解構後的響應式。 - 保持單一來源、遵守命名慣例,讓團隊協作更順暢。
有了這些概念與實作範例,你已經具備了在 Vue 3 中靈活運用 ref() 與 reactive() 的能力,接下來就可以把它們與 computed、watch、provide/inject 等其他 Composition API 結合,打造更強大、可擴充的前端應用。祝開發順利! 🚀