Vue ref 與 reactive
ref 和 reactive 是 Vue 3 Composition API 中最常用的兩個響應式 API。了解它們的差異和使用時機是寫好 Vue 3 程式碼的關鍵。
ref
ref() 用於建立一個響應式的參考值(reference),可以包裝任何型別的值:
<script setup>
import { ref } from 'vue'
// 基本型別
const count = ref(0)
const message = ref('Hello Vue!')
const isActive = ref(true)
// 物件
const user = ref({
name: 'John',
age: 30
})
// 陣列
const items = ref(['apple', 'banana', 'orange'])
</script>
存取 ref 的值
在 JavaScript 中,需要透過 .value 來存取和修改 ref 的值:
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 讀取
console.log(count.value) // 0
// 修改
count.value++
console.log(count.value) // 1
// 對於物件 ref
const user = ref({ name: 'John' })
user.value.name = 'Jane' // 修改物件屬性
user.value = { name: 'Bob' } // 替換整個物件
</script>
在模板中,Vue 會自動解包 ref,不需要使用 .value:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: 'John' })
</script>
<template>
<!-- 模板中不需要 .value -->
<p>{{ count }}</p>
<p>{{ user.name }}</p>
<button @click="count++">+1</button>
</template>
ref 用於 DOM 元素
ref 也可以用來取得 DOM 元素的參考:
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
// 元件掛載後,inputRef.value 會是 DOM 元素
inputRef.value.focus()
})
</script>
<template>
<input ref="inputRef" type="text">
</template>
更多 Template Refs 的用法請參考 Template Refs。
reactive
reactive() 用於建立一個響應式的物件:
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
},
items: ['apple', 'banana']
})
</script>
存取 reactive 的值
reactive 不需要 .value,直接存取屬性即可:
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: 'John' }
})
// 直接存取和修改
console.log(state.count) // 0
state.count++
state.user.name = 'Jane'
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.user.name }}</p>
</template>
reactive 的限制
- 只能用於物件型別
import { reactive } from 'vue'
// ❌ 不能用於基本型別
const count = reactive(0) // 不會是響應式
// ✅ 包裝在物件中
const state = reactive({ count: 0 })
- 不能替換整個物件
let state = reactive({ count: 0 })
// ❌ 會失去響應性
state = reactive({ count: 1 })
// ✅ 修改屬性
state.count = 1
- 解構會失去響應性
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 解構後的變數不是響應式的
let { count, name } = state
count++ // 不會觸發更新
ref vs reactive 比較
| 特性 | ref | reactive |
|---|---|---|
| 支援的型別 | 任何型別 | 只有物件型別 |
| 存取方式 | 需要 .value | 直接存取 |
| 替換整個值 | ✅ 可以 | ❌ 會失去響應性 |
| 解構 | 需要注意 | 會失去響應性 |
| 模板中自動解包 | ✅ | N/A |
何時使用 ref?
- 基本型別的值(string、number、boolean)
- 需要替換整個值的情況
- DOM 元素參考
- 單一的獨立值
// ✅ 適合使用 ref
const count = ref(0)
const isLoading = ref(false)
const selectedId = ref(null)
const inputRef = ref(null)
何時使用 reactive?
- 一組相關的資料
- 類似於元件的 data 物件
- 不需要替換整個物件的情況
// ✅ 適合使用 reactive
const form = reactive({
username: '',
email: '',
password: ''
})
const mouse = reactive({
x: 0,
y: 0
})
實務建議
許多 Vue 開發者傾向統一使用 ref,因為:
- 不需要記住什麼型別用什麼 API
- 明確的
.value讓程式碼更清楚哪些是響應式資料 - 可以任意替換整個值
<script setup>
import { ref } from 'vue'
// 統一使用 ref
const count = ref(0)
const user = ref({ name: 'John', age: 30 })
const items = ref(['a', 'b', 'c'])
// 可以任意替換
user.value = { name: 'Jane', age: 25 }
items.value = ['x', 'y', 'z']
</script>
響應式工具函式
toRef
從 reactive 物件中取出單一屬性並保持響應性:
import { reactive, toRef } from 'vue'
const state = reactive({
foo: 1,
bar: 2
})
// 建立一個連結到 state.foo 的 ref
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
也可以用於 props:
<script setup>
import { toRef } from 'vue'
const props = defineProps(['foo'])
// 建立一個響應式的 ref,會追蹤 props.foo 的變化
const fooRef = toRef(props, 'foo')
</script>
toRefs
將 reactive 物件的所有屬性轉為 ref:
import { reactive, toRefs } from 'vue'
const state = reactive({
foo: 1,
bar: 2
})
// 解構但保持響應性
const { foo, bar } = toRefs(state)
foo.value++
console.log(state.foo) // 2
這在 composable 函式中很常用:
// useCounter.js
import { reactive, toRefs } from 'vue'
export function useCounter() {
const state = reactive({
count: 0,
doubled: computed(() => state.count * 2)
})
function increment() {
state.count++
}
// 返回時用 toRefs,讓使用者可以解構
return {
...toRefs(state),
increment
}
}
// 使用
const { count, doubled, increment } = useCounter()
// count 和 doubled 仍然是響應式的
toValue(Vue 3.3+)
將 ref 或 getter 函式轉為普通值:
import { ref, toValue } from 'vue'
const count = ref(1)
const getter = () => 2
toValue(count) // 1
toValue(getter) // 2
toValue(3) // 3
這在寫 composable 時很有用,讓參數可以接受 ref、getter 或普通值:
import { toValue } from 'vue'
// 參數可以是 ref、getter 或普通值
function useFeature(input) {
// 統一取值
const value = toValue(input)
}
unref
unref 是 toValue 的舊版本,只能處理 ref:
import { ref, unref } from 'vue'
const count = ref(1)
unref(count) // 1
unref(1) // 1
// 等同於
val = isRef(val) ? val.value : val
實際範例
表單狀態管理
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({
username: '',
email: '',
password: ''
})
const isValid = computed(() => {
return form.username.length >= 3 &&
form.email.includes('@') &&
form.password.length >= 6 &&
form.password === form.confirmPassword
})
function validate() {
errors.username = form.username.length < 3 ? '使用者名稱至少 3 個字元' : ''
errors.email = !form.email.includes('@') ? '請輸入有效的 Email' : ''
errors.password = form.password.length < 6 ? '密碼至少 6 個字元' : ''
}
function submit() {
validate()
if (isValid.value) {
console.log('提交表單', form)
}
}
</script>
<template>
<form @submit.prevent="submit">
<div>
<input v-model="form.username" placeholder="使用者名稱">
<span class="error">{{ errors.username }}</span>
</div>
<div>
<input v-model="form.email" type="email" placeholder="Email">
<span class="error">{{ errors.email }}</span>
</div>
<div>
<input v-model="form.password" type="password" placeholder="密碼">
<span class="error">{{ errors.password }}</span>
</div>
<div>
<input v-model="form.confirmPassword" type="password" placeholder="確認密碼">
</div>
<button type="submit" :disabled="!isValid">送出</button>
</form>
</template>
計數器(比較兩種寫法)
使用 ref:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
</script>
使用 reactive:
<script setup>
import { reactive, computed } from 'vue'
const state = reactive({
count: 0
})
const doubled = computed(() => state.count * 2)
function increment() {
state.count++
}
function decrement() {
state.count--
}
function reset() {
state.count = 0
}
</script>
混合使用
實務上,通常會根據資料的特性混合使用:
<script setup>
import { ref, reactive, computed } from 'vue'
// 單一值用 ref
const isLoading = ref(false)
const error = ref(null)
const selectedId = ref(null)
// 相關資料用 reactive
const filters = reactive({
search: '',
category: 'all',
sortBy: 'date'
})
// 列表資料用 ref(可能需要替換整個陣列)
const items = ref([])
// computed 取決於使用的資料
const filteredItems = computed(() => {
return items.value.filter(item => {
if (filters.category !== 'all' && item.category !== filters.category) {
return false
}
if (filters.search && !item.name.includes(filters.search)) {
return false
}
return true
})
})
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
items.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
</script>