Vue3 – 響應式系統(Reactivity System)
主題:shallowRef 與 shallowReactive
簡介
在 Vue 3 中,響應式系統 由 Proxy 實作,讓開發者能以最直覺的方式追蹤資料變化。大部份情況下,我們會使用 ref()、reactive() 讓物件或原始值全域「深度」追蹤(deep‑track)。然而在實務開發裡,深度追蹤 並非總是最佳選擇:
- 效能成本 – 大型或巢狀結構的物件每一次變動都會觸發 Proxy,可能造成不必要的重新渲染。
- 外部庫的相容性 – 某些第三方庫(如圖表、地圖)內部已自行管理狀態,若再讓 Vue 包裝成深度響應式,會導致不可預期的行為。
shallowRef 與 shallowReactive 正是為了在 「只需要追蹤最外層」 時提供更輕量、效能友好的解決方案。本篇文章將從概念切入,透過實作範例說明如何正確使用這兩個 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 個實用案例,說明 何時使用 shallowRef、shallowReactive,以及 如何配合 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️⃣:動態載入的插件系統(同時使用 shallowRef 與 shallowReactive)
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.assign、spread 或直接重新賦值 (ref.value = newObj)。 |
把 shallowRef 當作普通變數 |
直接讀寫 .value 之外的屬性會失效。 |
確保所有存取都經過 .value(例如 myRef.value.prop),不要把 myRef 當作普通物件。 |
混用 reactive 與 shallowReactive |
同一物件若先 reactive 再 shallowReactive,會失去「淺層」的效果。 |
建議在建立階段就決定使用哪一種 API,避免二次包裝。 |
在模板中直接使用 shallowRef |
Vue 會自動解包 ref,但對 shallowRef 仍然需要 .value,否則會渲染 RefImpl 物件。 |
在 <template> 中直接寫 myShallowRef(Vue 會自動解包),但在 JavaScript 中務必使用 .value。 |
忘記在 watch 中設定 deep: true |
若需要監聽內層變化,shallowRef/shallowReactive 本身不支援深度監聽。 |
使用 watch(() => myRef.value, ...) 或自行在變更時手動觸發。 |
最佳實踐
- 先評估資料規模:若資料量 > 5,000 筆或結構深度 > 3 層,優先考慮
shallowRef/shallowReactive。 - 保持單一來源:在同一層級只使用一種響應式 API,避免混用造成預期外的行為。
- 手動觸發更新:在需要「局部」重新渲染時,直接改變最外層引用(
ref.value = {...})或使用triggerRef(Vue 3.3+)。 - 配合 TypeScript:使用
shallowRef<T>()、shallowReactive<T>()明確標註類型,可避免因為「any」導致的隱性錯誤。 - 測試效能:在開發階段使用 Chrome DevTools 的「Performance」或 Vue Devtools 的「Component」面板,觀察 Proxy 數量與更新頻率,確保淺層 API 真正減少了不必要的追蹤。
實際應用場景
- 大型列表或分頁資料:一次抓取大量資料,僅在切換頁面或重新載入時更新整個陣列。
- 第三方 UI 元件:如
ECharts、Mapbox、Quill等,它們自行管理內部狀態,僅需要外層配置的變更通知。 - 跨框架共用狀態:在微前端或混合框架(React + Vue)情境下,使用
shallowRef包裝外部傳入的物件,避免 Proxy 與其他框架衝突。 - Pinia Store 中的外部資源:例如 Firebase、Supabase、Auth0 的使用者物件,直接使用
shallowRef只關注「是否登入」這個布林值。 - 插件或模組系統:動態載入的插件往往包含龐大程式碼與設定,使用
shallow系列可以在不影響插件內部邏輯的前提下,仍保有 Vue 的響應式優勢。
總結
shallowRef與shallowReactive為 Vue 3 提供了「淺層」的響應式能力,讓開發者能在效能與相容性之間取得更好的平衡。- 核心概念:只追蹤第一層變更,內層不會自動觸發更新;若要更新必須 重新指派外層引用。
- 實務上,它們特別適合大型資料、第三方庫、跨框架共享狀態等情境,能顯著降低 Proxy 數量與記憶體佔用。
- 在使用時,避免「內層變動自動更新」的誤解,並遵循 單一來源、手動觸發、效能測試 等最佳實踐。
掌握了 shallowRef 與 shallowReactive,你就能在 Vue 3 的響應式系統中,彈性選擇追蹤粒度,寫出既高效又易維護的應用程式。祝開發順利,玩得開心! 🚀