本文 AI 產出,尚未審核

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 規則。
混用 refreadonly readonly(refValue) 只會返回只讀的 ref 本身,而不是其 .value 若要只讀 ref 的值,應使用 readonly(toRef(...)) 或自行封裝 getter。
忘記在 watch 中指定深度 觀測 shallowReactive 的深層屬性時,需要手動設定 deep: true 或監測具體屬性。 使用 watch(() => obj.nested.prop, ...)watch(obj, ..., { deep: true })

最佳實踐

  1. 只在需要共享的狀態上使用 readonly,避免過度保護導致過度封裝。
  2. 對大型資料結構採用 shallowReactive,並配合 watch 或手動觸發更新,以取得最佳效能。
  3. 在 TypeScript 中使用 Readonly<T> 配合 Vue 的 readonly(),提升編譯期安全性。
  4. 將只讀與可變分離:建立「私有」的 mutable 物件(如 _state),只暴露只讀介面給外部。

實際應用場景

  1. 全域設定(Config)

    • 只讀的設定檔在整個應用中共享,避免任意組件改寫。
    • 使用 shallowReactive 讓切換環境(dev ↔ prod)只需要改變第一層屬性。
  2. 第三方 UI 元件的資料傳遞

    • 如 Chart.js、Mapbox 等庫接受大量資料,使用 shallowReactive 減少 Proxy 效能成本。
  3. 多頁面表單的暫存資料

    • 表單資料往往層級較深,僅需要監控表單狀態(isDirtyisSubmitting),其餘欄位可保持非響應式,提升渲染速度。
  4. API 回傳的快取層

    • 把 API 回傳的結果包裝成 readonly(ref),防止 UI 不小心改寫快取,造成資料不一致。
  5. 插件開發

    • 插件向外暴露的 API 常使用 readonly,確保使用者只能透過插件提供的 method 進行變更,提升插件的穩定性。

總結

readonly()shallowReactive() 是 Vue 3 Composition API 中兩把利器

  • readonly():提供只讀保護,讓共享狀態更安全,特別適合全域設定、API 快取與插件介面。
  • shallowReactive():以淺層響應式降低大資料結構的效能開銷,常用於表格、圖表、或需要手動控制深層更新的情境。

透過正確的組合與最佳實踐,我們可以在 保證資料正確性 的同時,提升效能,讓 Vue 3 應用更具彈性與可維護性。希望本篇文章能幫助你在日常開發中更自信地使用這兩個 API,寫出更乾淨、更高效的程式碼。祝開發順利! 🚀