本文 AI 產出,尚未審核

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>

說明countref,在 <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>

說明reactiveform 內所有屬性自動成為響應式,v-model 直接綁定即可。


範例 3:混合使用 refreactive(物件包在 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。 養成習慣:只在 setupwatchcomputed 等純 JS 區塊使用 .value,模板中則可直接使用。
解構會失去響應式 const { foo } = reactiveObj 會產生普通變數,失去追蹤。 使用 toRefs()toRef() 取得保留響應式的 Ref。
混用 refreactive 產生雙重代理 把已是 reactive 的物件再包在 ref,會產生不必要的 Proxy,影響效能。 只使用其中一個:若需要深層代理,直接 reactive;若只想保留引用,使用 ref 包裝。
陣列變更不觸發更新(舊版 Vue 2 的問題) 在 Vue 3 中已不會發生,但仍需注意使用 push/pop 等變更陣列的方法時,仍要透過 .value 確保對 ref 包裝的陣列使用 .value,或直接使用 reactive 陣列。
對同一資料同時使用 refreactive 會導致兩套獨立的依賴圖,更新時可能不一致。 統一管理:在同一層級僅選擇 refreactive,或使用 shallowRef/shallowReactive 針對需求做區分。

最佳實踐

  1. 保持單一來源:在同一個模組或組件內,盡量只使用 ref reactive,避免混雜造成維護困難。
  2. 使用 toRefs 解構:在需要把 reactive 物件解構傳遞給子組件時,使用 toRefs 以保留響應式。
  3. 適度使用 shallowRef / shallowReactive:若只想讓外層變化可觀測,而內層保持普通物件,可使用這兩個「淺層」API,提升效能。
  4. 型別提示(TypeScript):ref<number>(0)reactive<User>({...}) 能讓編譯器協助捕捉錯誤。
  5. 保持一致的命名慣例:慣例上,ref 變數常以 xxxRef 或直接語意名稱(如 count)命名;reactive 變數則以描述性名稱(如 formprofile)命名。

實際應用場景

場景 使用 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 應用時寫出 更乾淨、可維護且效能友好 的程式碼。記得:

  1. 根據資料結構選擇 API(單值 → ref、物件 → reactive)。
  2. 在 JavaScript 區塊內務必使用 .value,或使用 toRefs / toRef 保留解構後的響應式。
  3. 保持單一來源、遵守命名慣例,讓團隊協作更順暢。

有了這些概念與實作範例,你已經具備了在 Vue 3 中靈活運用 ref()reactive() 的能力,接下來就可以把它們與 computedwatchprovide/inject 等其他 Composition API 結合,打造更強大、可擴充的前端應用。祝開發順利! 🚀