本文 AI 產出,尚未審核

Vue3 – 響應式系統(Reactivity System)

主題:shallowRefshallowReactive


簡介

在 Vue 3 中,響應式系統 由 Proxy 實作,讓開發者能以最直覺的方式追蹤資料變化。大部份情況下,我們會使用 ref()reactive() 讓物件或原始值全域「深度」追蹤(deep‑track)。然而在實務開發裡,深度追蹤 並非總是最佳選擇:

  1. 效能成本 – 大型或巢狀結構的物件每一次變動都會觸發 Proxy,可能造成不必要的重新渲染。
  2. 外部庫的相容性 – 某些第三方庫(如圖表、地圖)內部已自行管理狀態,若再讓 Vue 包裝成深度響應式,會導致不可預期的行為。

shallowRefshallowReactive 正是為了在 「只需要追蹤最外層」 時提供更輕量、效能友好的解決方案。本篇文章將從概念切入,透過實作範例說明如何正確使用這兩個 API,並分享常見陷阱與最佳實踐,讓你在開發大型 Vue 專案時能更靈活地掌控響應式的粒度。


核心概念

1. shallowRef 是什麼?

  • 定義shallowRef(value) 會回傳一個 只在第一層 具備 .value 讀寫的 Ref。
  • 行為:當你改變 .value 本身(例如指向另一個物件),Vue 會觸發更新;但 物件內部的屬性變動 不會被追蹤。
import { shallowRef } from 'vue'

const user = shallowRef({ name: 'Alice', age: 25 })

// 只要改變 .value 本身,就會觸發重繪
user.value = { name: 'Bob', age: 30 }   // ✅ 觸發

// 下面的寫法不會觸發更新,因為只是修改內部屬性
user.value.age = 31                     // ❌ 不會觸發

使用時機:需要把「大型資料」或「第三方庫返回的物件」交給 Vue 管理,但不希望每個屬性變動都觸發重新渲染。


2. shallowReactive 是什麼?

  • 定義shallowReactive(object) 會把 第一層屬性 包裝成響應式,內層仍保持原始的非代理狀態。
  • 行為:對第一層屬性讀寫會觸發依賴更新;若屬性本身是物件,對該物件的深層屬性改動則不會被追蹤。
import { shallowReactive } from 'vue'

const state = shallowReactive({
  list: [],          // 第一層是響應式的
  config: { theme: 'light' } // config 本身是響應式,但裡面的 theme 不是
})

// 下面會觸發更新
state.list.push(1)          // ✅ 觸發
state.list = [2, 3]         // ✅ 觸發

// 下面不會觸發(深層變動)
state.config.theme = 'dark' // ❌ 不會觸發

使用時機:在 Vuex / Pinia 或組件內部狀態管理時,需要快速建立大量「只要被替換」的資料結構,且不想為每個子屬性建立 Proxy。


3. 為什麼要「淺層」?

項目 ref / reactive(深度) shallowRef / shallowReactive(淺層)
效能 每個屬性都會產生 Proxy,深層變動都會觸發 只在第一層建立 Proxy,減少記憶體與追蹤成本
相容性 可能與第三方庫內部的 Proxy 衝突 保持原始物件結構,避免衝突
使用情境 小型或中等資料結構、需要細粒度監控 大型列表、外部 API 回傳、需要手動觸發更新的場景
更新方式 任意屬性變動即觸發 必須改變最外層引用或第一層屬性才會觸發

程式碼範例

以下示範 5 個實用案例,說明 何時使用 shallowRefshallowReactive,以及 如何配合 Vue 3 的 Composition API。

範例 1️⃣:大型表格資料的快取(使用 shallowRef

import { shallowRef, watchEffect } from 'vue'
import axios from 'axios'

export default {
  setup() {
    // 大型表格一次抓取 10,000 筆資料
    const tableData = shallowRef([])

    // 手動觸發更新的函式
    const fetchData = async () => {
      const { data } = await axios.get('/api/large-table')
      tableData.value = data   // 只改變最外層引用,觸發渲染
    }

    // 初始載入
    fetchData()

    // 監聽外部變數(例如 filter)並重新抓取
    watchEffect(() => {
      // 假設有 filter 改變時重新抓取
      // filter 改變時會自動呼叫 fetchData()
    })

    return { tableData, fetchData }
  }
}

說明tableData 內部可能包含上千個物件,若使用 reactive 會為每個屬性建立 Proxy,耗費大量記憶體。shallowRef 只在 .value 被替換時觸發更新,符合「一次抓取、整批更新」的需求。


範例 2️⃣:第三方圖表庫的配置(使用 shallowReactive

import { shallowReactive, onMounted, watch } from 'vue'
import Chart from 'chart.js/auto'

export default {
  setup() {
    const chartOptions = shallowReactive({
      type: 'bar',
      data: {
        labels: [],      // 只要改變整體 data 就會重繪
        datasets: []    // 深層變動不會觸發
      },
      options: {
        responsive: true
      }
    })

    let chartInstance = null

    onMounted(() => {
      const ctx = document.getElementById('myChart')
      chartInstance = new Chart(ctx, chartOptions) // 直接傳入 shallowReactive
    })

    // 若要更新圖表,必須重新指派 data
    const updateData = (newData) => {
      chartOptions.data = newData   // ✅ 觸發 Chart 更新
    }

    return { chartOptions, updateData }
  }
}

說明:Chart.js 本身會自行監聽 data 變化,若 Vue 為每個 datasets 內的屬性都建立 Proxy,容易造成「雙重監聽」或效能問題。shallowReactive 只讓 data 本身具備響應式,配合圖表的 API 重新指派即可。


範例 3️⃣:Pinia Store 中的外部 API 物件(使用 shallowRef

// stores/user.js
import { defineStore } from 'pinia'
import { shallowRef } from 'vue'
import { getCurrentUser } from '@/api/auth' // 回傳 Firebase User 物件

export const useUserStore = defineStore('user', {
  state: () => ({
    // Firebase User 物件內部非常龐大且自行管理
    currentUser: shallowRef(null)
  }),
  actions: {
    async loadUser() {
      const user = await getCurrentUser()
      this.currentUser = user      // 只改變引用,觸發組件更新
    }
  }
})

說明:Firebase 的 User 物件本身已具備觀察機制,若使用 reactive 會與 Firebase 的內部實作衝突。shallowRef 讓 Pinia 只負責「是否有使用者」的存在與否,而不干涉其內部屬性。


範例 4️⃣:表單暫存(使用 shallowReactive

import { shallowReactive, toRefs } from 'vue'

export default {
  setup() {
    const draft = shallowReactive({
      title: '',
      content: '',
      meta: {
        tags: []   // meta 本身是響應式的,但 tags 裡的每個字串不需要追蹤
      }
    })

    const submit = () => {
      // 只要把 draft 整體送出即可
      console.log('Submit:', JSON.parse(JSON.stringify(draft)))
    }

    // 若需要手動觸發重新渲染,可改變最外層屬性
    const reset = () => {
      draft.title = ''
      draft.content = ''
    }

    return { ...toRefs(draft), submit, reset }
  }
}

說明:表單的 meta.tags 只是一個純陣列,裡面的字串不需要被 Vue 追蹤,使用 shallowReactive 可省去不必要的 Proxy,減少記憶體消耗。


範例 5️⃣:動態載入的插件系統(同時使用 shallowRefshallowReactive

import { shallowRef, shallowReactive, watchEffect } from 'vue'

export default {
  setup() {
    // 插件列表會在執行時載入
    const plugins = shallowRef([])               // 只追蹤列表本身

    // 每個插件的設定只需要第一層響應式
    const pluginSettings = shallowReactive({})   // key: pluginId => 設定物件

    // 動態載入插件
    const loadPlugin = async (url) => {
      const mod = await import(url)
      plugins.value.push(mod.default)            // 觸發列表更新
      pluginSettings[mod.id] = { enabled: true } // 觸發設定更新
    }

    // 監聽插件列表變化,做額外的初始化工作
    watchEffect(() => {
      plugins.value.forEach(p => {
        if (p.init) p.init()
      })
    })

    return { plugins, pluginSettings, loadPlugin }
  }
}

說明:插件本身可能是大型函式庫,使用 shallowRef 只追蹤「是否載入」;而每個插件的設定只需要第一層屬性(如 enabled),因此使用 shallowReactive 即可。


常見陷阱與最佳實踐

陷阱 說明 解決方式
誤以為內層變動會自動更新 shallowRef/shallowReactive 只追蹤第一層,開發者常忘記必須重新指派外層才能觸發渲染。 使用 Object.assignspread 或直接重新賦值 (ref.value = newObj)。
shallowRef 當作普通變數 直接讀寫 .value 之外的屬性會失效。 確保所有存取都經過 .value(例如 myRef.value.prop),不要把 myRef 當作普通物件。
混用 reactiveshallowReactive 同一物件若先 reactiveshallowReactive,會失去「淺層」的效果。 建議在建立階段就決定使用哪一種 API,避免二次包裝。
在模板中直接使用 shallowRef Vue 會自動解包 ref,但對 shallowRef 仍然需要 .value,否則會渲染 RefImpl 物件。 <template> 中直接寫 myShallowRef(Vue 會自動解包),但在 JavaScript 中務必使用 .value
忘記在 watch 中設定 deep: true 若需要監聽內層變化,shallowRef/shallowReactive 本身不支援深度監聽。 使用 watch(() => myRef.value, ...) 或自行在變更時手動觸發。

最佳實踐

  1. 先評估資料規模:若資料量 > 5,000 筆或結構深度 > 3 層,優先考慮 shallowRefshallowReactive
  2. 保持單一來源:在同一層級只使用一種響應式 API,避免混用造成預期外的行為。
  3. 手動觸發更新:在需要「局部」重新渲染時,直接改變最外層引用(ref.value = {...})或使用 triggerRef(Vue 3.3+)。
  4. 配合 TypeScript:使用 shallowRef<T>()shallowReactive<T>() 明確標註類型,可避免因為「any」導致的隱性錯誤。
  5. 測試效能:在開發階段使用 Chrome DevTools 的「Performance」或 Vue Devtools 的「Component」面板,觀察 Proxy 數量與更新頻率,確保淺層 API 真正減少了不必要的追蹤。

實際應用場景

  1. 大型列表或分頁資料:一次抓取大量資料,僅在切換頁面或重新載入時更新整個陣列。
  2. 第三方 UI 元件:如 EChartsMapboxQuill 等,它們自行管理內部狀態,僅需要外層配置的變更通知。
  3. 跨框架共用狀態:在微前端或混合框架(React + Vue)情境下,使用 shallowRef 包裝外部傳入的物件,避免 Proxy 與其他框架衝突。
  4. Pinia Store 中的外部資源:例如 Firebase、Supabase、Auth0 的使用者物件,直接使用 shallowRef 只關注「是否登入」這個布林值。
  5. 插件或模組系統:動態載入的插件往往包含龐大程式碼與設定,使用 shallow 系列可以在不影響插件內部邏輯的前提下,仍保有 Vue 的響應式優勢。

總結

  • shallowRefshallowReactive 為 Vue 3 提供了「淺層」的響應式能力,讓開發者能在效能與相容性之間取得更好的平衡。
  • 核心概念:只追蹤第一層變更,內層不會自動觸發更新;若要更新必須 重新指派外層引用
  • 實務上,它們特別適合大型資料、第三方庫、跨框架共享狀態等情境,能顯著降低 Proxy 數量與記憶體佔用。
  • 在使用時,避免「內層變動自動更新」的誤解,並遵循 單一來源、手動觸發、效能測試 等最佳實踐。

掌握了 shallowRefshallowReactive,你就能在 Vue 3 的響應式系統中,彈性選擇追蹤粒度,寫出既高效又易維護的應用程式。祝開發順利,玩得開心! 🚀