Vue3 — 響應式系統(Reactivity System)
markRaw、toRaw、isRef、isReactive
簡介
在 Vue 3 中,響應式系統是整個框架的核心。它負責追蹤資料的變化,進而觸發視圖更新。大多數開發者只會使用 reactive()、ref() 這兩個最常見的 API,卻不太了解 markRaw、toRaw、isRef、isReactive 這四個輔助函式的用途與意義。
markRaw可以告訴 Vue「這個物件不需要被轉成響應式」,適用於大型第三方庫或頻繁變動的資料結構。toRaw則是 把已被 Vue 包裝過的代理(proxy)物件還原成原始值,在需要直接操作原始資料或做深度比較時非常有用。isRef與isReactive分別用來 檢測一個值是否為ref或 是否已被reactive包裝,在寫通用的組件或工具函式時能避免不必要的錯誤。
掌握這四個 API,能讓你在開發大型專案時更靈活、更有效率,也能避免常見的效能陷阱。接下來,我們會一步步拆解概念、示範實作,並提供實務上的最佳實踐。
核心概念
1. markRaw – 把物件排除在響應式追蹤之外
Vue 內部會使用 Proxy 包裝所有傳入 reactive()、ref() 的值,讓它們在讀寫時自動收集依賴並觸發更新。然而,有時候我們不想讓某個物件被 Proxy 包裝,原因可能包括:
- 物件本身已經是 immutable(如常量、凍結的物件)。
- 物件是 大型第三方庫的實例(如
Map、Set、或自訂的圖形庫),頻繁的 Proxy 會拖慢效能。 - 需要 保持原始參照,避免在深層比較時產生不必要的變化。
使用方式
import { markRaw, reactive } from 'vue'
const heavyLibInstance = new SomeHeavyLibrary()
// 把它標記為「不需要被追蹤」
// 後續放入 reactive 結構中仍保持原始參照
const state = reactive({
lib: markRaw(heavyLibInstance),
count: 0
})
// 改變 count 仍會觸發視圖更新
state.count++
// lib 本身不會被 Proxy 包裝,也不會觸發依賴收集
console.log(state.lib === heavyLibInstance) // true
重點:
markRaw只在建立 reactive 結構時生效,之後即使把該物件傳遞給其他組件,Vue 也不會再將它轉成 Proxy。
2. toRaw – 從 Proxy 取回原始物件
當我們拿到一個已被 Vue 包裝的 Proxy(例如 reactive() 的返回值),有時需要取得未被代理的原始資料。常見情境包括:
- 深度相等比較(
lodash.isEqual、JSON.stringify)需要原始結構。 - 第三方函式庫只能接受「純」物件,若傳入 Proxy 會拋出錯誤。
- 快取機制需要使用原始參照作為鍵值。
使用方式
import { reactive, toRaw } from 'vue'
const raw = { a: 1 }
const proxy = reactive(raw)
// proxy 與 raw 看起來相同,但實際上是不同的物件
console.log(proxy === raw) // false
// 透過 toRaw 取得原始物件
const original = toRaw(proxy)
console.log(original === raw) // true
注意:
toRaw只能在 開發環境或工具函式 中使用,不要在模板或渲染函式 直接呼叫,否則會失去 Vue 的依賴追蹤。
3. isRef – 判斷值是否為 Ref
ref() 用於建立「單一值」的響應式容器,與 reactive() 不同,它返回的是一個 帶有 .value 屬性的物件。在撰寫通用組件或自訂 Hook 時,我們常常需要先判斷傳入的參數是否已經是 ref,以免重複包裝。
使用方式
import { ref, isRef } from 'vue'
function unwrap(value) {
// 若是 ref,回傳 .value;否則直接回傳原始值
return isRef(value) ? value.value : value
}
const count = ref(0)
const plain = 10
console.log(unwrap(count)) // 0
console.log(unwrap(plain)) // 10
isRef 只會在 ref() 包裝過的物件 上回傳 true,不會把 reactive 物件或普通的 JavaScript 物件誤判。
4. isReactive – 判斷物件是否已被 reactive 包裝
在大型應用中,我們可能會在不同層級傳遞同一個資料物件。若不確定它是否已經是 Proxy,使用 isReactive 可以避免 重複包裝(會造成「雙層 Proxy」的效能損耗)。
使用方式
import { reactive, isReactive } from 'vue'
const obj = { msg: 'Hello' }
const proxy1 = reactive(obj)
console.log(isReactive(obj)) // false
console.log(isReactive(proxy1)) // true
// 防止二次包裝
const maybeProxy = isReactive(obj) ? obj : reactive(obj)
程式碼範例(實用示例)
以下提供 5 個常見情境,示範 markRaw、toRaw、isRef、isReactive 的實際應用。每個範例都附有說明與最佳實踐提示。
範例 1:大型圖表套件的整合(使用 markRaw)
import { reactive, markRaw } from 'vue'
import Chart from 'chart.js' // 假設是個重量級套件
export default {
setup() {
const chartInstance = new Chart(/* config */)
// 把 Chart 實例標記為 raw,避免 Vue 追蹤其內部屬性
const state = reactive({
chart: markRaw(chartInstance),
data: [10, 20, 30]
})
// 更新資料時只需要觸發 Vue 更新,不會因 chart 被 Proxy 而產生額外開銷
function addData(value) {
state.data.push(value)
state.chart.update()
}
return { state, addData }
}
}
最佳實踐:只在 建立階段 使用
markRaw,之後不要再對同一個實例呼叫reactive,以免產生不可預期的行為。
範例 2:深層比較前取回原始物件(使用 toRaw)
import { reactive, toRaw } from 'vue'
import isEqual from 'lodash/isEqual'
const store = reactive({
user: {
id: 1,
name: 'Alice',
preferences: { theme: 'dark' }
}
})
// 假設有一個快取機制,需要比較前後兩次的 user 物件
function hasUserChanged(prev) {
// prev 可能是從快取中取出的「原始」物件
const currentRaw = toRaw(store.user)
return !isEqual(prev, currentRaw)
}
// 初次快取
let cachedUser = JSON.parse(JSON.stringify(toRaw(store.user)))
// 後續檢查
if (hasUserChanged(cachedUser)) {
console.log('使用者資料有變動')
cachedUser = JSON.parse(JSON.stringify(toRaw(store.user)))
}
注意:使用
JSON.stringify只適合 純資料,若有函式或 Symbol,請改用lodash.cloneDeep或其他深拷貝工具。
範例 3:自訂 Hook 必須支援 ref 與普通值(使用 isRef)
import { ref, isRef, watchEffect } from 'vue'
/**
* 監聽一個值的變化,支援普通值或 ref
* @param {any|Ref} source
* @param {Function} callback
*/
function useWatch(source, callback) {
if (isRef(source)) {
watchEffect(() => callback(source.value))
} else {
// 普通值不具備 reactivity,直接呼叫一次
callback(source)
}
}
// 使用範例
const count = ref(0)
useWatch(count, (val) => console.log('count 變成', val))
useWatch(42, (val) => console.log('固定值', val))
技巧:若你的 Hook 必須同時接受
reactive物件,建議使用isReactive搭配isRef進行更細緻的類型判斷。
範例 4:防止二次 Proxy(使用 isReactive)
import { reactive, isReactive } from 'vue'
function ensureReactive(obj) {
// 若已是 reactive,直接回傳;否則包裝一次
return isReactive(obj) ? obj : reactive(obj)
}
const raw = { name: 'Bob' }
const first = ensureReactive(raw) // 產生 Proxy
const second = ensureReactive(first) // 不會再產生新 Proxy
console.log(first === second) // true
建議:在 store、service 層或 第三方插件 中使用此函式,能保證資料只被包裝一次,降低記憶體與效能開銷。
範例 5:結合四個 API 的完整範例
import { reactive, ref, markRaw, toRaw, isRef, isReactive } from 'vue'
import lodash from 'lodash'
// 假設有一個外部 API 回傳的資料結構
const apiResult = {
meta: { timestamp: Date.now() },
payload: { items: [1, 2, 3] },
lib: new lodash.Collection() // 大型第三方物件
}
// 1. 把第三方物件排除追蹤
apiResult.lib = markRaw(apiResult.lib)
// 2. 建立響應式狀態
const state = reactive({
meta: apiResult.meta,
payload: apiResult.payload,
lib: apiResult.lib
})
// 3. 透過 ref 包裝需要單獨追蹤的值
state.currentItem = ref(state.payload.items[0])
// 4. 在需要比較時取回原始值
function isPayloadChanged(prevRaw) {
const curRaw = toRaw(state.payload)
return !lodash.isEqual(prevRaw, curRaw)
}
// 5. 使用 isRef / isReactive 做安全檢查
function logInfo(key) {
const value = state[key]
if (isRef(value)) {
console.log(`${key} 是 ref,值為`, value.value)
} else if (isReactive(value)) {
console.log(`${key} 是 reactive 物件,內容為`, value)
} else {
console.log(`${key} 為普通值`, value)
}
}
logInfo('currentItem') // ref
logInfo('payload') // reactive
logInfo('lib') // 普通 (已被 markRaw)
重點回顧:
markRaw讓第三方實例不被 Proxy 包裝。toRaw在比較或傳遞給外部函式前還原原始資料。isRef、isReactive為類型安全護欄,避免不必要的錯誤。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方式 / 最佳實踐 |
|---|---|---|
誤把 markRaw 用在普通資料 |
失去 Vue 的自動更新能力,UI 不會重新渲染 | 僅在確定不需要追蹤的物件上使用,例如第三方類別實例或已凍結的常量 |
在模板中直接呼叫 toRaw |
失去依賴收集,導致畫面不會更新 | 只在 setup / composable 中使用,或在需要「純」資料時才呼叫 |
把 reactive 物件傳給只能接受普通物件的函式(如某些圖形庫) |
函式拋錯或行為異常 | 事先使用 toRaw 轉成原始物件 |
忘記檢查 isRef 而直接使用 .value |
若傳入普通值會拋出 undefined 錯誤 |
使用 isRef 包裝存取或利用 unref()(Vue 內建) |
重複包裝同一物件(reactive 再 reactive) |
產生雙層 Proxy,效能下降且 isReactive 判斷會錯誤 |
先用 isReactive 判斷,或使用 ensureReactive 之類的工具函式 |
最佳實踐小結:
- 明確規劃資料的響應式層級:先決定哪些資料需要追蹤,哪些不需要,然後再使用
reactive、ref、markRaw。 - 在工具函式中加入類型檢查:使用
isRef、isReactive防止誤用。 - 保持 Proxy 與原始資料的分離:在與外部庫交互時,使用
toRaw把資料還原,避免意外的 Proxy 參數。 - 避免在渲染階段呼叫
toRaw:保持依賴收集的完整性。 - 使用 TypeScript 時:可搭配
Ref<T>、UnwrapRef<T>以及DeepReadonly<T>讓編譯器協助捕捉錯誤。
實際應用場景
1. 大型表格或圖表套件
當你在 Vue 中使用 AG-Grid、ECharts、Highcharts 等重量級套件時,常常需要把圖表實例存放在狀態中卻不希望 Vue 追蹤其內部屬性。此時 markRaw 是最佳選擇,能避免不必要的 Proxy 開銷,同時仍保持圖表的即時更新(透過手動呼叫 update())。
2. 快取與資料同步
在 離線同步 或 資料快取 的情境下,往往需要把最新的資料和快取的舊資料做深度比較。使用 toRaw 能確保比較的對象是「純粹」的 JS 物件,而不受 Proxy 包裝的影響。
3. 通用組件庫
如果你在開發 自訂 UI 元件庫(例如一套表單元件),使用者可能會傳入 ref、普通值或已經是 reactive 的物件。此時在組件內部使用 isRef、isReactive 進行類型判斷,能讓元件在不同使用情境下保持一致的行為。
4. 第三方 API 整合
許多外部 API(如 Firebase、Socket.io)回傳的物件本身已經具備自己的變更機制。將這些物件直接放入 Vue 狀態時,使用 markRaw 可以避免 Vue 重新包裝,減少不必要的依賴收集與記憶體占用。
5. 動態插件系統
在 插件化架構 中,插件可能會在執行時動態注入功能或資料結構。插件提供者不一定了解 Vue 的響應式機制,這時在核心層使用 isReactive、isRef 來檢測與包裝,能保證插件不會因重複 Proxy 而導致效能下降。
總結
Vue 3 的響應式系統提供了 reactive、ref 之外的四個重要工具:
markRaw:讓你把不需要追蹤的物件「排除」在 Proxy 之外,提升效能。toRaw:在需要「純」資料時把 Proxy 還原,避免深層比較或外部 API 出錯。isRef:檢測是否為ref,在撰寫通用 Hook 時提供安全保護。isReactive:判斷物件是否已被reactive包裝,防止二次 Proxy。
掌握這四個 API,能讓你在 大型專案、第三方套件整合、快取與同步 等情境下,靈活控制 Vue 的追蹤行為,避免常見的效能陷阱,同時寫出更可維護、可擴充的程式碼。
實務建議:在專案的 state 建構階段(如 Pinia store、Vuex module)就明確決定哪些屬性需要
reactive、哪些需要markRaw;在 通用函式或組件 中使用isRef、isReactive做類型保護,最後在 與外部庫交互 時,適時使用toRaw把資料還原。遵循這套流程,你的 Vue 3 應用將更具效能與可預測性。祝開發順利!