Vue3 Composition API(核心)── 回傳 template context
簡介
在 Vue 3 中,Composition API 讓我們可以把組件的邏輯抽離成可重用的函式,而 setup() 則是所有邏輯的入口。setup() 執行完畢後,Vue 會將 返回值(return value) 作為 template context,也就是模板中可以直接使用的變數、函式或計算屬性。
掌握「回傳 template context」的細節,不僅能讓模板保持簡潔、易讀,還能避免常見的 reactivity 失效或命名衝突問題。因此,這篇文章會從概念說明、實作範例、常見陷阱與最佳實踐,一直到實務應用,完整介紹如何在 Composition API 中正確地把資料提供給模板使用。
核心概念
1. setup() 的返回值會自動映射到模板
export default {
setup() {
const count = ref(0) // reactive 狀態
const inc = () => count.value++ // 方法
// 只要把需要在模板中使用的變數/函式回傳,
// Vue 會把它們掛到模板的作用域上
return { count, inc }
}
}
在上述範例中,{{ count }} 會自動解包 ref,顯示 count.value 的值;@click="inc" 也能直接呼叫 inc。
重點:返回的物件會被 reactive 包裝(即使是普通物件),因此在模板中使用時會自動保持響應式。
2. ref、reactive 與 toRefs 的差異
| 方法 | 說明 | 在模板中的使用方式 |
|---|---|---|
ref() |
包裝單一值或物件,須以 .value 讀寫 |
自動解包({{ myRef }})或在 JS 中使用 .value |
reactive() |
深度響應式的物件 | 直接使用屬性({{ state.name }}) |
toRefs() |
把 reactive 物件的每個屬性轉成 ref,避免解構時失去響應式 |
同 ref,在解構後仍保有響應式 |
範例 1:使用 reactive + toRefs
export default {
setup() {
const state = reactive({
name: 'Vue',
age: 3
})
// 若直接解構會失去響應式,必須使用 toRefs
const { name, age } = toRefs(state)
const increaseAge = () => age.value++
return { name, age, increaseAge }
}
}
<!-- template -->
<div>
<p>名稱:{{ name }}</p>
<p>年齡:{{ age }}</p>
<button @click="increaseAge">長大一年</button>
</div>
3. computed 也是回傳給模板的常見類型
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
return { firstName, lastName, fullName }
}
}
<p>全名:{{ fullName }}</p>
computed 會在依賴變更時自動重新計算,且在模板中同樣會自動解包。
4. defineExpose(<script setup>)讓子組件顯式暴露 context
在 <script setup> 中,所有在 setup() 內定義的變數預設會曝光給模板,但若想讓父組件透過 ref 取得子組件的內部方法,需使用 defineExpose:
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const reset = () => (count.value = 0)
defineExpose({ reset }) // 暴露給父組件
</script>
<template>
<p>計數:{{ count }}</p>
</template>
父組件:
<template>
<Child ref="childRef" />
<button @click="childRef?.reset()">重設子組件計數</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
</script>
5. script setup 的隱式返回
使用 <script setup> 時,不需要手動 return,所有在頂層聲明的 ref、reactive、computed、函式,都會自動成為模板的 context:
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const qty = ref(2)
const total = computed(() => price.value * qty.value)
</script>
<template>
<p>總價:{{ total }}</p>
</template>
這讓程式碼更簡潔,也減少忘記 return 的錯誤。
程式碼範例(實用示例)
範例 1:表單雙向綁定 + 表單驗證
<script setup>
import { ref, computed } from 'vue'
const form = reactive({
email: '',
password: ''
})
// 簡易驗證規則
const emailError = computed(() =>
form.email.includes('@') ? '' : '請輸入有效的 Email'
)
const passwordError = computed(() =>
form.password.length >= 6 ? '' : '密碼至少 6 個字元'
)
const canSubmit = computed(() => !emailError.value && !passwordError.value)
function submitForm() {
if (canSubmit.value) {
alert(`送出資料:${JSON.stringify(form)}`)
}
}
</script>
<template>
<form @submit.prevent="submitForm">
<label>
Email:
<input v-model="form.email" type="email" />
</label>
<span class="error">{{ emailError }}</span>
<label>
Password:
<input v-model="form.password" type="password" />
</label>
<span class="error">{{ passwordError }}</span>
<button :disabled="!canSubmit">送出</button>
</form>
</template>
說明:所有的
reactive、computed、function都直接在模板中使用,沒有額外的return,因為<script setup>會自動把它們加入 context。
範例 2:使用 toRefs 讓解構保持響應式
export default {
setup() {
const user = reactive({
name: 'Alice',
age: 25
})
// 正確做法:使用 toRefs
const { name, age } = toRefs(user)
const birthday = () => age.value++
return { name, age, birthday }
}
}
<div>
<p>姓名:{{ name }}</p>
<p>年齡:{{ age }}</p>
<button @click="birthday">慶祝生日</button>
</div>
陷阱:如果直接寫
const { name, age } = user,name、age會變成普通值,模板不會再更新。
範例 3:子組件暴露方法給父組件(defineExpose)
<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
function inc() { count.value++ }
function reset() { count.value = 0 }
defineExpose({ inc, reset })
</script>
<template>
<p>計數:{{ count }}</p>
</template>
<!-- Parent.vue -->
<template>
<Counter ref="counterRef" />
<button @click="counterRef?.inc()">+1</button>
<button @click="counterRef?.reset()">重設</button>
</template>
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'
const counterRef = ref(null)
</script>
關鍵:
defineExpose只在<script setup>中使用,讓父組件可以安全地呼叫子組件內部的函式。
範例 4:動態列表與 key 的最佳實踐
export default {
setup() {
const items = ref([
{ id: 1, text: '第一項' },
{ id: 2, text: '第二項' }
])
const addItem = () => {
const nextId = items.value.length + 1
items.value.push({ id: nextId, text: `第 ${nextId} 項` })
}
const removeItem = (id) => {
items.value = items.value.filter(item => item.id !== id)
}
return { items, addItem, removeItem }
}
}
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
<button @click="removeItem(item.id)">刪除</button>
</li>
</ul>
<button @click="addItem">新增項目</button>
說明:
items為ref包裹的陣列,返回後直接在模板使用v-for,而:key必須使用唯一的id,才能讓 Vue 正確追蹤項目。
範例 5:使用 watch 監聽返回的變數
import { ref, watch } from 'vue'
export default {
setup() {
const query = ref('')
const results = ref([])
// 監聽 query 變化,模擬搜尋 API
watch(query, async (newVal) => {
if (newVal) {
// 假裝呼叫 API
results.value = await fakeApiSearch(newVal)
} else {
results.value = []
}
})
return { query, results }
}
}
async function fakeApiSearch(q) {
// 模擬延遲
return new Promise(resolve => {
setTimeout(() => resolve([`${q} 結果 1`, `${q} 結果 2`]), 500)
})
}
<input v-model="query" placeholder="輸入關鍵字" />
<ul>
<li v-for="r in results" :key="r">{{ r }}</li>
</ul>
技巧:
watch必須在setup()內部建立,且監聽的目標必須是返回給模板的ref或computed,才能保證資料流的完整性。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案 / 最佳實踐 |
|---|---|---|
| 忘記回傳需要的變數 | 模板報錯 Property or method "xxx" is not defined |
確認 setup() 最後 return 的物件包含所有模板需要的項目,或使用 <script setup> 免除手動 return。 |
直接解構 reactive 物件 |
失去響應式,畫面不會更新 | 使用 toRefs() 或 readonly(),或直接在模板中使用 state.prop。 |
在模板中直接使用 .value |
失去自動解包的便利,且在 <script setup> 中會造成不必要的冗餘 |
讓 Vue 自動解包,除非在 JavaScript 中需要讀寫 .value。 |
| 返回過大的物件 | 每次渲染都會重新建立,影響效能 | 只回傳「模板需要」的最小集合,其他可放在局部變數或 useXxx composable 中。 |
| 命名衝突(props vs return) | props 會被覆寫,導致預期外行為 | 若需要同名,使用 ...props 展開後自行重新命名,或避免同名。 |
在 <script setup> 中使用 defineExpose 時忘記導入 |
defineExpose 未被辨識,編譯錯誤 |
確保 import { defineExpose } from 'vue'(Vue 3.3+)或直接使用全域 defineExpose(已自動注入)。 |
最佳實踐清單
- 最小化返回:只回傳模板真正需要的狀態與方法。
- 使用
toRefs:在需要解構reactive時,務必使用toRefs保持響應式。 - 保持命名一致:盡量避免在
props、setup返回值與data中使用相同名稱。 - 利用
script setup:簡化語法、降低忘記return的機會。 - 適時使用
defineExpose:在需要父組件直接呼叫子組件方法時才使用,避免過度暴露內部實作。 - 測試 reactivity:開發時使用 Vue Devtools 檢查返回的變數是否為
ref/reactive,確保更新能即時反映。
實際應用場景
表單與驗證
- 多欄位表單的狀態、錯誤訊息與提交函式全部放在
setup(),返回給模板,讓 UI 與驗證邏輯清晰分離。
- 多欄位表單的狀態、錯誤訊息與提交函式全部放在
資料表格與分頁
setup()內部管理currentPage、pageSize、sortedData,返回給模板後配合watch或computed產生即時的分頁結果。
彈窗/對話框的開關狀態
- 使用
ref控制isOpen,以及open()、close()方法返回,讓父子組件透過v-model或expose輕鬆控制。
- 使用
跨組件共享邏輯(Composable)
- 把共用的資料抓取、緩存、錯誤處理封裝成
useFetch(),在各個組件的setup()中呼叫,僅返回需要渲染的data、error、loading。
- 把共用的資料抓取、緩存、錯誤處理封裝成
動畫與過渡控制
ref保存動畫狀態(如isAnimating),返回後在模板中使用v-if、v-show或transition組件,確保動畫與資料同步。
總結
- 回傳 template context 是 Vue 3 Composition API 的核心機制之一。只要在
setup()中正確返回需要的ref、reactive、computed與方法,模板就能直接使用,且保持完整的響應式特性。 - 使用
toRefs、defineExpose、以及<script setup>,能讓程式碼更安全、易讀與維護。 - 透過本篇提供的概念說明與實作範例,你可以在表單、列表、跨組件共享、以及父子組件互動等多種情境中,靈活運用「回傳 template context」的技巧。
- 最後,別忘了 最小化返回內容、避免解構失效,以及 善用 Vue Devtools 觀察 reactivity,這樣才能寫出既高效又易除錯的 Vue 3 應用程式。
祝開發順利,玩得開心! 🎉