Vue3 Composition API(核心)
readonly() 與 shallowReactive()
簡介
在 Vue 3 中,Composition API 提供了更彈性的狀態管理方式,讓我們可以在 setup() 內自由組合邏輯。除了最常見的 reactive()、ref(),還有兩個常被忽視但相當實用的 API:readonly() 與 shallowReactive()。
readonly()用來 建立只能讀取的響應式物件,避免在大型專案中不小心修改共享狀態,提升程式的可預測性與可維護性。shallowReactive()則是 只對第一層屬性進行響應式轉換,在處理大量資料或深層巢狀結構時,可顯著降低效能開銷。
本篇文章將深入說明這兩個 API 的運作原理、使用時機、常見陷阱與最佳實踐,並提供實務範例,幫助你在開發 Vue3 應用時寫出更安全、更高效的程式碼。
核心概念
1. readonly() – 建立不可變的響應式物件
readonly() 接收一個已有的響應式物件(或普通的 JavaScript 物件),回傳一個只讀版。在開發過程中,如果嘗試對只讀物件寫入值,Vue 會在開發模式下拋出警告,而在生產模式則會靜默失敗。
import { reactive, readonly } from 'vue'
const state = reactive({
count: 0,
user: { name: 'Alice', age: 30 }
})
// 建立只讀版
const readOnlyState = readonly(state)
// 正常讀取
console.log(readOnlyState.count) // 0
// 嘗試修改 → 開發環境會警告
readOnlyState.count = 10 // ❗[Vue warn] Set operation on a key "count" failed: target is readonly
為什麼要使用只讀?
- 防止意外變更:大型專案中,狀態往往被多個組件共享。只讀保護讓「資料來源」只能在特定模組中被修改,降低 bug 產生的機率。
- 提升可測試性:測試時可以直接以只讀狀態作為輸入,確保測試不會改變全域狀態。
2. shallowReactive() – 淺層響應式
shallowReactive() 與 reactive() 的差異在於 只對第一層屬性做 Proxy 包裝,深層物件仍保持原始的非響應式狀態。這對於以下情境非常有用:
- 大型列表或深度巢狀資料(如樹狀結構)僅關心外層屬性的變化。
- 想要在不影響子物件的情況下,快速切換整個資料結構的「可觀測」狀態。
import { shallowReactive } from 'vue'
const deepData = {
id: 1,
meta: {
created: new Date(),
tags: ['vue', 'reactivity']
}
}
// 只對第一層做響應式
const shallow = shallowReactive(deepData)
// 改變第一層屬性 → 會觸發更新
shallow.id = 2 // ✅ 觸發
// 改變深層屬性 → 不會觸發
shallow.meta.tags.push('javascript') // ❌ 不會觸發
何時選擇 shallowReactive?
- 大量資料:對整個資料樹做深層 Proxy 會產生較大的記憶體與效能負擔,
shallowReactive可減少不必要的追蹤。 - 外部庫或非 Vue 管理的物件:若資料由第三方庫產生且已自行實作變更通知,僅需讓外層可觀測即可。
3. readonly() 與 shallowReactive() 的組合
有時候我們希望 只讀且只對第一層可觀測,可以直接將兩者結合:
import { shallowReactive, readonly } from 'vue'
const config = shallowReactive({
apiUrl: 'https://api.example.com',
headers: { Authorization: 'Bearer xxx' }
})
// 只讀版的淺層響應式
const readOnlyConfig = readonly(config)
// 只能讀取外層屬性,且寫入會被阻止
console.log(readOnlyConfig.apiUrl) // 正常
readOnlyConfig.apiUrl = 'https://new.example.com' // 警告
程式碼範例
以下提供 5 個實用範例,展示如何在日常開發中運用 readonly() 與 shallowReactive()。
範例 1:全域狀態的只讀封裝
// store.js
import { reactive, readonly } from 'vue'
const _state = reactive({
theme: 'light',
user: { id: 1, name: 'Bob' }
})
// 只暴露只讀介面給組件
export const state = readonly(_state)
// 在需要修改的地方,仍保留私有的 mutation 方法
export function setTheme(theme) {
_state.theme = theme
}
重點:組件只能
import { state },無法直接改變theme,必須透過setTheme。
範例 2:表格資料的淺層響應式
import { shallowReactive, watch } from 'vue'
const tableData = shallowReactive({
rows: [], // 大量資料
sortKey: '',
sortOrder: 'asc'
})
// 只關心排序欄位變化
watch(
() => tableData.sortKey,
(newKey) => {
// 重新排序 rows(此處自行實作排序演算法)
console.log('排序欄位改變為', newKey)
}
)
// 改變排序欄位會觸發 watch
tableData.sortKey = 'name'
說明:
rows內部的每筆資料若是深層物件,改變其屬性不會觸發watch,減少不必要的計算。
範例 3:只讀的 API 回傳結果
import { ref, readonly } from 'vue'
import axios from 'axios'
export function useUser(id) {
const data = ref(null)
const loading = ref(false)
async function fetch() {
loading.value = true
const res = await axios.get(`/api/users/${id}`)
data.value = res.data
loading.value = false
}
// 只讀回傳,避免外部直接改變
return {
user: readonly(data),
loading: readonly(loading),
fetch
}
}
實務應用:API 回傳的資料通常不應被任意修改,使用
readonly能保證資料的完整性。
範例 4:淺層響應式結合第三方圖表庫
import { shallowReactive, onMounted, watch } from 'vue'
import Chart from 'chart.js'
export default {
setup() {
const chartOption = shallowReactive({
type: 'bar',
data: {
labels: [], // 由外部動態注入
datasets: [] // 由外部動態注入
},
options: { responsive: true }
})
let chartInstance = null
onMounted(() => {
const ctx = document.getElementById('myChart').getContext('2d')
chartInstance = new Chart(ctx, chartOption) // Chart.js 只會觀察第一層
})
// 當外部更新 labels 時,手動呼叫 chart.update()
watch(() => chartOption.data.labels, () => {
chartInstance && chartInstance.update()
})
}
}
重點:Chart.js 本身不支援 Vue 的深層 Proxy,使用
shallowReactive能避免不必要的 Proxy 開銷,同時仍能透過手動watch觸發更新。
範例 5:只讀與淺層結合的設定檔
import { shallowReactive, readonly } from 'vue'
const rawConfig = {
apiBase: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
}
// 只對第一層做響應式,且整體只讀
export const config = readonly(shallowReactive(rawConfig))
// 任何嘗試改寫的行為都會在開發環境警告
// config.timeout = 8000 // ❗ 警告
應用:在多環境(dev / prod)切換時,只需要改變
rawConfig本身,而不必擔心組件會意外改寫設定值。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
將普通物件直接傳給 readonly() |
若傳入的不是已經是 reactive 的物件,readonly 仍會返回一個 Proxy,但內部屬性不具備響應式特性。 |
先使用 reactive() 或 shallowReactive() 包裝,再交給 readonly()。 |
深層變更未被追蹤(使用 shallowReactive) |
只監聽第一層,深層屬性變更不會觸發更新,可能導致 UI 不同步。 | 若需要深層追蹤,改用 reactive();若只關心外層,搭配 watch 手動觀測深層變化。 |
| 在生產環境忽略警告 | readonly 在生產環境不會拋出錯誤,只是靜默失敗,開發者可能不易發現問題。 |
在開發階段使用 npm run dev,確保所有只讀寫入都被警告;可自行加入 lint 規則。 |
混用 ref 與 readonly |
readonly(refValue) 只會返回只讀的 ref 本身,而不是其 .value。 |
若要只讀 ref 的值,應使用 readonly(toRef(...)) 或自行封裝 getter。 |
忘記在 watch 中指定深度 |
觀測 shallowReactive 的深層屬性時,需要手動設定 deep: true 或監測具體屬性。 |
使用 watch(() => obj.nested.prop, ...) 或 watch(obj, ..., { deep: true })。 |
最佳實踐:
- 只在需要共享的狀態上使用
readonly,避免過度保護導致過度封裝。 - 對大型資料結構採用
shallowReactive,並配合watch或手動觸發更新,以取得最佳效能。 - 在 TypeScript 中使用
Readonly<T>配合 Vue 的readonly(),提升編譯期安全性。 - 將只讀與可變分離:建立「私有」的 mutable 物件(如
_state),只暴露只讀介面給外部。
實際應用場景
全域設定(Config)
- 只讀的設定檔在整個應用中共享,避免任意組件改寫。
- 使用
shallowReactive讓切換環境(dev ↔ prod)只需要改變第一層屬性。
第三方 UI 元件的資料傳遞
- 如 Chart.js、Mapbox 等庫接受大量資料,使用
shallowReactive減少 Proxy 效能成本。
- 如 Chart.js、Mapbox 等庫接受大量資料,使用
多頁面表單的暫存資料
- 表單資料往往層級較深,僅需要監控表單狀態(
isDirty、isSubmitting),其餘欄位可保持非響應式,提升渲染速度。
- 表單資料往往層級較深,僅需要監控表單狀態(
API 回傳的快取層
- 把 API 回傳的結果包裝成
readonly(ref),防止 UI 不小心改寫快取,造成資料不一致。
- 把 API 回傳的結果包裝成
插件開發
- 插件向外暴露的 API 常使用
readonly,確保使用者只能透過插件提供的 method 進行變更,提升插件的穩定性。
- 插件向外暴露的 API 常使用
總結
readonly() 與 shallowReactive() 是 Vue 3 Composition API 中兩把利器:
readonly():提供只讀保護,讓共享狀態更安全,特別適合全域設定、API 快取與插件介面。shallowReactive():以淺層響應式降低大資料結構的效能開銷,常用於表格、圖表、或需要手動控制深層更新的情境。
透過正確的組合與最佳實踐,我們可以在 保證資料正確性 的同時,提升效能,讓 Vue 3 應用更具彈性與可維護性。希望本篇文章能幫助你在日常開發中更自信地使用這兩個 API,寫出更乾淨、更高效的程式碼。祝開發順利! 🚀