Vue 響應式原理
響應式(Reactivity)是 Vue 最核心的特性之一。當你修改響應式資料時,Vue 會自動追蹤變化並更新相關的 DOM,讓你不需要手動操作 DOM。
什麼是響應式?
簡單來說,響應式就是「資料改變時,畫面自動更新」:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++ // 修改資料
// 畫面會自動更新,不需要手動操作 DOM
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
Vue 3 響應式系統原理
Vue 3 使用 JavaScript Proxy 來實現響應式系統,這比 Vue 2 使用的 Object.defineProperty 更強大、更完整。
Proxy 的優勢
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 監聽物件屬性新增/刪除 | ❌ 不支援 | ✅ 支援 |
| 監聽陣列索引變化 | ❌ 不支援 | ✅ 支援 |
| 監聽 Map/Set | ❌ 不支援 | ✅ 支援 |
| 效能 | 遞迴遍歷所有屬性 | 惰性處理 |
簡化的響應式原理
// 這是簡化後的概念說明
// Vue 使用 Proxy 攔截物件的讀取和寫入操作
const handler = {
get(target, key) {
track(target, key) // 追蹤依賴
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 觸發更新
return true
}
}
const proxy = new Proxy(data, handler)
當你讀取響應式資料時,Vue 會「追蹤」這個讀取操作,記錄下誰在使用這個資料;當你修改資料時,Vue 會「觸發」更新,通知所有使用這個資料的地方重新執行。
響應式 API
Vue 3 提供了幾個核心的響應式 API:
ref()
ref() 用於建立一個響應式的值,可以是任何型別:
<script setup>
import { ref } from 'vue'
// 基本型別
const count = ref(0)
const message = ref('Hello')
// 在 JavaScript 中需要用 .value 存取
console.log(count.value) // 0
count.value++
// 在模板中不需要 .value
</script>
<template>
<p>{{ count }}</p>
<p>{{ message }}</p>
</template>
reactive()
reactive() 用於建立一個響應式的物件:
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
}
})
// 不需要 .value
state.count++
state.user.name = 'Jane'
</script>
<template>
<p>{{ state.count }}</p>
<p>{{ state.user.name }}</p>
</template>
更多 ref 和 reactive 的用法請參考 ref 與 reactive。
響應式的限制
只對物件型別有效
reactive() 只對物件型別(Object、Array、Map、Set)有效,不能用於基本型別(string、number、boolean):
import { reactive } from 'vue'
// ❌ 不行!基本型別無法使用 reactive
const count = reactive(0)
// ✅ 使用 ref 包裝基本型別
import { ref } from 'vue'
const count = ref(0)
不能替換整個物件
替換整個響應式物件會失去響應性:
import { reactive } from 'vue'
let state = reactive({ count: 0 })
// ❌ 這會失去響應性!
state = reactive({ count: 1 })
// 之前的 state 已經和模板脫鉤了
正確做法是修改物件的屬性:
// ✅ 修改屬性
state.count = 1
// ✅ 或使用 Object.assign
Object.assign(state, { count: 1, name: 'new' })
解構會失去響應性
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 解構後失去響應性
let { count, name } = state
count++ // 這不會更新畫面
// ✅ 使用 toRefs 保持響應性
const { count, name } = toRefs(state)
count.value++ // 這會更新畫面
深層響應式
預設情況下,ref() 和 reactive() 都會深層轉換物件內的所有巢狀屬性:
<script setup>
import { ref, reactive } from 'vue'
const obj = ref({
nested: {
count: 0
},
arr: ['foo', 'bar']
})
// 深層屬性也是響應式的
obj.value.nested.count++
obj.value.arr.push('baz')
const state = reactive({
deep: {
deeper: {
value: 'hello'
}
}
})
// 深層修改也會觸發更新
state.deep.deeper.value = 'world'
</script>
淺層響應式
如果你不需要深層響應式,可以使用 shallowRef() 或 shallowReactive():
import { shallowRef, shallowReactive } from 'vue'
// 只有 .value 的變化是響應式的
const shallow = shallowRef({
nested: { count: 0 }
})
shallow.value.nested.count++ // ❌ 不會觸發更新
shallow.value = { nested: { count: 1 } } // ✅ 會觸發更新
// 只有根層屬性是響應式的
const state = shallowReactive({
foo: 1,
nested: { bar: 2 }
})
state.foo++ // ✅ 響應式
state.nested.bar++ // ❌ 不是響應式
淺層響應式通常用於效能優化,當你有很大的物件但只需要追蹤根層變化時。
DOM 更新時機
當你修改響應式資料時,DOM 更新是非同步的。Vue 會等到「下一個 tick」才執行 DOM 更新,以確保無論你修改了多少資料,每個元件只需要更新一次。
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 還沒更新
console.log(document.getElementById('count').textContent) // 舊值
// 等待 DOM 更新完成
await nextTick()
// DOM 已更新
console.log(document.getElementById('count').textContent) // 新值
}
</script>
<template>
<p id="count">{{ count }}</p>
<button @click="increment">+1</button>
</template>
偵錯響應式
Vue 提供了一些工具來偵錯響應式系統:
isRef() 和 isReactive()
import { ref, reactive, isRef, isReactive } from 'vue'
const count = ref(0)
const state = reactive({ count: 0 })
console.log(isRef(count)) // true
console.log(isRef(state)) // false
console.log(isReactive(state)) // true
console.log(isReactive(count)) // false
toRaw()
取得響應式物件的原始物件:
import { reactive, toRaw } from 'vue'
const state = reactive({ count: 0 })
const raw = toRaw(state)
console.log(raw === state) // false
console.log(raw) // { count: 0 } - 普通物件
這在你需要傳遞資料給外部函式庫,或不想觸發響應式追蹤時很有用。
markRaw()
標記物件永遠不要轉成響應式:
import { reactive, markRaw } from 'vue'
const foo = markRaw({ count: 0 })
const state = reactive({ foo })
// foo 不會被轉成響應式
console.log(isReactive(state.foo)) // false
這在你有大型物件但不需要響應式追蹤時很有用,例如第三方類別的實例。
響應式與 computed
computed 會自動追蹤它所依賴的響應式資料:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// fullName 會追蹤 firstName 和 lastName
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 當 firstName 或 lastName 改變時,fullName 會自動重新計算
firstName.value = 'Jane'
console.log(fullName.value) // 'Jane Doe'
</script>
更多 computed 的用法請參考 Computed 計算屬性。
響應式與 watch
watch 可以監聽響應式資料的變化:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`count 從 ${oldValue} 變成 ${newValue}`)
})
</script>
更多 watch 的用法請參考 Watch 監聽器。
效能考量
- 避免不必要的響應式:不需要追蹤變化的資料不要用
ref或reactive - 使用 shallowRef/shallowReactive:大型物件只需追蹤根層變化時
- 使用 markRaw:第三方物件或不變的資料
- 避免在響應式物件中存放大量資料:考慮只存放必要的 ID,而不是整個物件
// ❌ 不好:存放大量資料在響應式物件中
const items = ref(hugeArrayOfObjects)
// ✅ 好:只存放 ID,需要時再查詢
const selectedIds = ref([1, 2, 3])
const itemsMap = markRaw(new Map(hugeArrayOfObjects.map(item => [item.id, item])))