Vue3 Composition API(核心)
toRef()、toRefs()、unref()
簡介
在 Vue 3 中,Composition API 讓我們可以以函式的方式組織邏輯,提升程式碼的可重用性與可測試性。
當我們在 setup() 內使用 reactive() 產生一個「深層響應式」的物件時,往往需要把其中的某些屬性解構(destructure)或傳遞給其他函式。若直接解構,原本的響應式連結會被斷開,導致 UI 不會更新。
toRef()、toRefs() 與 unref() 正是為了解決 「從 reactive 物件取得單一屬性、保持響應式」 以及 「從 ref 取得原始值」 這兩個常見需求而設計的工具函式。掌握它們的使用方式,能讓你在開發大型 Vue 專案時,避免許多微妙的 bug,並寫出更乾淨、可維護的程式碼。
核心概念
1. ref 與 reactive 的差別
ref(value) |
reactive(object) |
|
|---|---|---|
| 用途 | 包裝單一值(原始值或物件) | 包裝整個物件,深層轉成響應式 |
| 取得值方式 | myRef.value |
直接使用屬性 state.count |
| 常見情境 | 表單欄位、計數器、簡單布林 | 複雜資料結構、API 回傳的 JSON |
Tip:
reactive內部會自動把每個屬性轉成ref,但它本身不是ref,因此不能直接使用.value。
2. 為什麼需要 toRef()、toRefs()
當我們把 reactive 物件解構成普通變數時:
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // ❌ count、name 失去響應式
此時 count、name 成為普通的 JavaScript 變數,Vue 再也無法追蹤它們的變化。toRef() 與 toRefs() 正是用來 保持單個屬性的響應式連結,讓解構後仍能自動更新 UI。
3. toRef(source, key?)
參數
source:reactive物件或ref包裝的物件。key(可選):要取得的屬性名稱,若省略則回傳整個source本身的ref。
回傳:一個指向
source[key]的 ref,即使source本身變化,這個 ref 仍會保持同步。
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count') // countRef === ref(0)
countRef.value++ // state.count 也會跟著變
4. toRefs(source)
- 參數:
reactive物件或ref包裝的物件。 - 回傳:一個新物件,裡面的每個屬性都是對應
source中屬性的 ref。
這是將 整個物件 同時「轉成可解構的 refs」的快捷方式。
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // count、name 仍是 ref
count.value++ // state.count 會同步變化
注意:
toRefs()只會對第一層屬性產生 ref,深層物件仍保持原本的reactive行為。
5. unref(value)
- 參數:可能是
ref、reactive、或普通值。 - 回傳:如果傳入的是
ref,則回傳其.value;否則直接回傳原始值。
在模板或計算屬性裡,我們常會寫 someRef.value,但在 JavaScript 中有時希望自動「解包」:
function double(v) {
return unref(v) * 2
}
const count = ref(5)
console.log(double(count)) // 10
console.log(double(3)) // 6
程式碼範例
以下示範 5 個常見情境,從基礎到實務應用,一步步說明 toRef()、toRefs()、unref() 的使用方式。
範例 1:單屬性保持響應式(toRef)
import { reactive, toRef, watch } from 'vue'
export default {
setup() {
const user = reactive({ name: 'Alice', age: 25 })
// 只想監聽 name,卻不想把整個物件傳給子組件
const nameRef = toRef(user, 'name')
// 監聽 name 的變化
watch(nameRef, (newName) => {
console.log('使用者名稱變更為', newName)
})
// 在其他地方修改
setTimeout(() => {
nameRef.value = 'Bob' // -> console 會印出變更訊息
}, 1000)
return { nameRef }
}
}
重點:
nameRef仍然是ref,所以子組件若直接接受nameRef,仍能保持雙向綁定。
範例 2:一次解構多屬性(toRefs)
import { reactive, toRefs } from 'vue'
export default {
setup() {
const form = reactive({
email: '',
password: '',
remember: false
})
// 直接解構,保持每個欄位都是 ref
const { email, password, remember } = toRefs(form)
// 送出表單前的驗證函式
const submit = () => {
if (!email.value.includes('@')) {
alert('Email 格式錯誤')
return
}
console.log('提交資料', {
email: email.value,
password: password.value,
remember: remember.value
})
}
return { email, password, remember, submit }
}
}
Tip:在模板中直接使用
password等變數,Vue 會自動解包.value,寫起來更乾淨。
範例 3:將 reactive 物件傳給子組件(toRefs + 解構)
<!-- Parent.vue -->
<template>
<Child v-bind="userRefs" />
</template>
<script setup>
import { reactive, toRefs } from 'vue'
import Child from './Child.vue'
const user = reactive({ id: 1, username: 'admin', role: 'editor' })
const userRefs = toRefs(user) // { id: Ref, username: Ref, role: Ref }
</script>
<!-- Child.vue -->
<template>
<div>
<p>使用者名稱: {{ username }}</p>
<p>角色: {{ role }}</p>
<button @click="changeName">改名為 Guest</button>
</div>
</template>
<script setup>
defineProps({
id: Number,
username: String,
role: String
})
// 因為父層傳來的是 Ref,直接改 .value 即可同步
function changeName() {
// `username` 其實是 Ref,Vue 會自動把它解包為 .value
// 但在 setup 中若要手動改,仍需 `.value`
username.value = 'Guest'
}
</script>
關鍵:父層使用
toRefs把reactive轉成「可解構的 refs」,子組件接受的仍是ref,因此雙向綁定不會斷。
範例 4:使用 unref 簡化通用函式
import { ref, unref } from 'vue'
function clamp(value, min, max) {
const v = unref(value) // 若是 ref,取 .value;否則直接使用
const lower = unref(min)
const upper = unref(max)
return Math.min(Math.max(v, lower), upper)
}
// 測試
const num = ref(15)
console.log(clamp(num, 0, 10)) // 10
console.log(clamp(5, 0, 10)) // 5
好處:
clamp可以接受ref或普通值,使用者不必在呼叫前自行.value,提升 API 的彈性。
範例 5:與 watchEffect 結合的技巧
import { reactive, toRefs, watchEffect, unref } from 'vue'
export default {
setup() {
const settings = reactive({
darkMode: false,
fontSize: 14
})
const { darkMode, fontSize } = toRefs(settings)
// 使用 unref 讓 watchEffect 更簡潔
watchEffect(() => {
const mode = unref(darkMode) ? 'dark' : 'light'
const size = unref(fontSize)
document.body.dataset.theme = mode
document.body.style.fontSize = `${size}px`
})
// 測試變更
setTimeout(() => {
darkMode.value = true
fontSize.value = 18
}, 2000)
return { darkMode, fontSize }
}
}
說明:
watchEffect內部自動追蹤ref,但若想在外部函式裡取得值,unref能讓程式碼保持「純值」的感覺。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 解構後失去響應式 | 直接 const { foo } = reactiveObj 會得到普通值。 |
使用 toRef(reactiveObj, 'foo') 或 toRefs(reactiveObj)。 |
| 多層物件仍是 reactive | toRefs 只會處理第一層,深層結構仍是 reactive,若再解構會斷連。 |
若需要深層解構,手動對每層使用 toRef,或考慮使用 shallowRef 包裝。 |
在模板外忘記 .value |
在 setup() 中使用 ref 時,必須寫 myRef.value;在模板裡則會自動解包。 |
盡量在 setup 中使用 unref 或 toRefs 讓變數直接是 ref,減少 .value 的出現。 |
將 ref 直接傳給非 Vue 函式 |
某些第三方函式期望普通值,傳入 ref 會出錯。 |
使用 unref 先取得原始值再傳入。 |
在 watch 中使用 toRef 產生的 ref |
若 watch 的第一個參數是 ref.value,會失去追蹤。 |
直接把 ref 本身作為第一個參數,或使用 watchEffect。 |
最佳實踐
- 盡量使用
toRefs解構:在setup中把reactive物件一次性轉成可解構的 refs,讓後續的代碼既簡潔又保持響應式。 - 在公共函式裡使用
unref:讓函式接受ref或普通值,提高 API 的彈性。 - 避免深層解構:如果真的需要深層解構,先把深層物件抽成獨立的
reactive或ref,再套用toRef。 - 命名慣例:對於
ref,建議在變數名稱後加上Ref(如countRef),或使用toRefs直接取得同名的ref(count),讓閱讀者一眼就能分辨。 - 使用 TypeScript 時:
toRefs會正確推斷類型,搭配defineProps、defineEmits可得到完整的型別提示。
實際應用場景
| 場景 | 為什麼需要 toRef / toRefs / unref |
|---|---|
| 表單元件的雙向綁定 | 父層使用 reactive 管理所有欄位,子元件只需要單一欄位的 ref,使用 toRef 傳遞即可保持同步。 |
| 跨組件共享狀態 | 透過 provide/inject 提供 reactive 物件,子組件使用 toRefs 解構後直接使用,避免手動 .value。 |
| 自訂 Hook / Composable | 內部使用 reactive 保存狀態,對外回傳 toRefs(state),讓使用者可以自由解構並直接在模板中使用。 |
| 第三方函式庫的 API | 如 lodash、date-fns 等純 JavaScript 函式只接受普通值,使用 unref 包裝可直接傳入 ref。 |
| 動態樣式或主題切換 | 把主題設定放在 reactive,使用 toRefs 把 darkMode、primaryColor 等屬性導出,watchEffect 結合 unref 即可即時更新 DOM。 |
總結
toRef(source, key):把reactive(或ref包裝的物件)中的單一屬性轉成 ref,保持響應式。toRefs(source):一次把整個reactive物件的第一層屬性全部轉成 ref,便於解構與傳遞。unref(value):自動取得ref的.value,若不是ref則直接回傳原值,讓函式更具彈性。
在 Vue 3 的 Composition API 中,這三個工具函式是 保持響應式完整性、提升程式碼可讀性與可重用性的關鍵。只要遵循上述最佳實踐,避免常見的解構斷鏈陷阱,就能寫出乾淨、易維護的 Vue 應用程式。祝開發順利,玩得開心! 🚀