Vue3響應式核心:ref vs reactive深度對比

前言
Vue3 提供了兩種創(chuàng)建響應式數(shù)據(jù)的方式:ref 和 reactive。它們有什么區(qū)別?在開發(fā)中該如何選擇?本文將詳細講解它們的用法、適用場景,并介紹相關的輔助 API,如:
shallowRef和shallowReactive(淺層響應式)triggerRef(手動觸發(fā) DOM 更新)customRef(自定義響應式邏輯)readonly(防止數(shù)據(jù)被修改)
讀完本文,你將徹底理解 Vue3 的響應式系統(tǒng),并能在項目中正確使用這些 API!
ref
?
ref接受任意類型值,返回響應式對象,通過.value訪問
需要注意的是被ref包裝之后需要.value 來進行取值或賦值,模版除外
比如:
<template>
<!-- 無需.value -->
<p>{{ name }}</p>
</template>
<script setup lang="ts">
const name = ref('南玖')
// 需要.value
name.value = 'nanjiu'
</script>接收任意值
?
ref可以接收基本類型、引用類型的數(shù)據(jù)以及DOM的ref的屬性值
const name = ref('南玖')
const obj = ref({
name: '南玖',
age: 20
})
console.log(name)
console.log(obj)
- 如果ref接收的是一個基本類型的數(shù)據(jù),那么
.value保存的就是就是該原始值 - 如果ref接收的是一個引用類型的數(shù)據(jù),那么
.value保存的就是代理了該引用數(shù)據(jù)的proxy對象 - 無論是基本數(shù)據(jù)類型還是引用數(shù)據(jù)類型,最終返回的都是由 RefImpl 類構造出來的對象
響應式
ref默認提供深層響應式,也就是說即使我們修改嵌套的引用類型數(shù)據(jù),vue也能夠檢測到并觸發(fā)頁面更新
<template>
<p>{{ num }}</p>
<button @click="num++">num++</button>
<p>{{ person.info.age }}</p>
<button @click="person.info.age++">age++</button>
</template>
<script setup lang="ts">
const num = ref(1)
const person = ref({
name: '鹿',
info: {
age: 20,
}
})
</script>
也就是說無論嵌套多深,vue都能夠監(jiān)聽到數(shù)據(jù)的變化,說到監(jiān)聽數(shù)據(jù)變化,這就得提一下watch方法了,雖然vue能夠監(jiān)聽到嵌套數(shù)據(jù)的變化,但是watch函數(shù)如果監(jiān)聽的是ref定義的引用類型數(shù)據(jù),默認是不會開啟深度監(jiān)聽的
<template>
<p>{{ person.info.age }}</p>
<button @click="person.info.age++">age++</button>
</template>
<script setup lang="ts">
const person = ref({
name: '鹿',
info: {
age: 20,
}
})
watch(() => person.value, (newValue, oldValue) => {
console.log('person changed from', oldValue, 'to', newValue)
})
</script>
雖然頁面視圖更新了,但是watch是無法監(jiān)聽到數(shù)據(jù)變化的,想要監(jiān)聽到這一變化,我們需要手動開啟深度監(jiān)聽
watch(() => person.value, (newValue, oldValue) => {
console.log('person changed from', oldValue, 'to', newValue)
}, {
deep: true // 深度監(jiān)聽
})
shallowRef
由于ref默認是深層響應式,但有時候我們?yōu)榱诵阅芸紤],也可以通過 shallowRef 來放棄深層響應性。對于淺層 ref,只有 .value 的訪問會被追蹤。
<template>
<p>ref: {{ person.info.age }}</p>
<button @click="person.info.age++">age++</button>
<p>shallowRef: {{ animal.age }}</p>
<button @click="animalAgeAdd">age++</button>
</template>
<script setup lang="ts">
const person = ref({
name: '鹿',
info: {
age: 20,
}
})
const animal = shallowRef({
name: '小鹿',
age: 5
})
const animalAgeAdd = () => {
// 修改淺響應式對象的屬性
animal.value.age++
console.log('animal age changed to', animal.value.age)
}
</script>
修改屬性值,雖然數(shù)據(jù)變化了,但是頁面并不會更新,并且無法通過watch監(jiān)聽數(shù)據(jù)變化。
??這里還有一點需要注意的是,ref與shallowRef最好不要一起使用,否則shallowRef會被影響
比如:
const animalAgeAdd = () => {
// 修改深響應式對象的屬性
person.value.info.age++
// 修改淺響應式對象的屬性
animal.value.age++
// 這樣會導致頁面上的animal.age 也會更新
}
triggerRef
?
強制觸發(fā)依賴于一個淺層 ref的副作用,這通常在對淺引用的內(nèi)部值進行深度變更后使用。
當一個淺層ref的屬性值發(fā)生改變又想觸發(fā)頁面更新時,可以手動調(diào)用triggerRef來實現(xiàn)
const animal = shallowRef({
name: '小鹿',
age: 5
})
const animalAgeAdd = () => {
// 修改淺響應式對象的屬性
animal.value.age++
triggerRef(animal) // 手動觸發(fā)更新
}customRef
?
創(chuàng)建一個自定義的 ref,顯式聲明對其依賴追蹤和更新觸發(fā)的控制方式。
customRef() 接收一個工廠函數(shù)作為參數(shù),該函數(shù)接收 track 和 trigger 兩個函數(shù)作為參數(shù),并返回一個帶有 get 和 set 方法的對象。
- track:用于收集依賴項。在
get方法中調(diào)用,收集該 ref 所依賴的響應式數(shù)據(jù)。 - trigger:用于觸發(fā)更新。在
set方法中調(diào)用,通知依賴項更新視圖。
const myRef = customRef((track, trigger) => {
let value = 0
return {
get() {
track()
return value
},
set(newValue) {
if (newValue !== value) {
value = newValue
trigger()
}
}
}
})
console.log(myRef)customRef允許我們通過獲取或設置一個變量的值時進行一些額外的操作,而不需要偵聽這個變量進行額外的操作。
比如,我們可以使用cusromRef實現(xiàn)一個自帶防抖的響應式數(shù)據(jù)
const useDebounceRef = (value: any, delay?: number) => {
return customRef((track, trigger) => {
let timer: ReturnType<typeof setTimeout>
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger()
console.log('value changed to', value)
}, delay || 100)
}
}
})
}
const inputValue = useDebounceRef('', 1000)reactive
?
reactive用于將一個引用類型數(shù)據(jù)聲明為響應式數(shù)據(jù),返回的是一個Proxy對象。
只接受引用類型數(shù)據(jù)
const car = reactive({
brand: 'GTR',
model: 'Corolla',
year: 2020,
info: {
color: 'red',
mileage: 15000
}
})
const carNum = reactive(100)
console.log('引用數(shù)據(jù)類型', car)
console.log('基本數(shù)據(jù)類型', carNum)
重要限制:reactive只接受對象類型,基本類型會原樣返回并產(chǎn)生警告
從上圖我們還能看到,正常使用的reactive返回的是一個Proxy對象,也就是說reactive 實現(xiàn)響應式就是基于ES6 Proxy 實現(xiàn)的。
響應式
與ref一樣,reactive默認也是深層響應式,并且watch的監(jiān)聽是默認開啟深度監(jiān)聽的
const car = reactive({
brand: 'GTR',
model: 'Corolla',
year: 2020,
info: {
color: 'red',
mileage: 15000,
total: 10
}
})
watch(car, (newValue, oldValue) => {
console.log('car changed from', oldValue.info.total, 'to', newValue.info.total)
})
會丟失響應式的幾個操作
- 對象引用發(fā)生變化
由于 Vue 的響應式跟蹤是通過屬性訪問實現(xiàn)的,因此必須始終保持對響應式對象的相同引用。
let person = reactive({
name: 'nanjiu'
})
// 重新賦值
person = {
name: '南玖22',
}
// 這里再修改數(shù)據(jù),頁面并不會更新
const changeNameProxy = () => {
person.name = '小鹿' // 修改代理對象的屬性
console.log('修改代理對象后', person) // Proxy(Object) {name: '小鹿'}
}
- 解構
當我們將響應式對象的原始類型屬性解構為本地變量時,或者將該屬性傳遞給函數(shù)時,也將丟失響應性
let person = reactive({
name: 'nanjiu'
})
let { name } = person
const changeNameProxy = () => {
name = '小鹿' // 修改解構后的屬性,頁面不會更新,person.name也不會更新
console.log('修改代理對象后', person) // Proxy(Object) {name: 'nanjiu'}
}原始對象與代理對象
reactive()返回的是一個原始對象的 Proxy代理對象,兩者是不相等的
const raw = {
name: '南玖'
}
const person = reactive(raw)
console.log('原始對象', raw)
console.log('響應式對象', person)
console.log('person === raw', person === raw) // false
- 原始對象與代理對象是相互影響的
const raw = {
name: '南玖'
}
const person = reactive(raw)
raw.name = '小鹿' // 修改原始對象的屬性
// person.name = '小鹿' // 修改響應式對象的屬性
console.log('原始對象', raw) // {name: '小鹿'}
console.log('響應式對象', person) // Proxy(Object) {name: '小鹿'}當原始對象里面的數(shù)據(jù)發(fā)生改變時,代理對象的數(shù)據(jù)也會發(fā)生變化;當代理對象里面的數(shù)據(jù)發(fā)生變化時,對應的原始數(shù)據(jù)也會發(fā)生變化
既然兩者可以相互影響,那么修改原始對象會不會觸發(fā)頁面更新呢???
答案是不會的,只有代理對象是響應式的,更改原始對象不會觸發(fā)更新。因此,使用 Vue 的響應式系統(tǒng)的最佳實踐是僅使用你聲明對象的代理版本。
代理一致性
為保證訪問代理的一致性,對同一個原始對象調(diào)用 reactive() 會總是返回同樣的代理對象,而對一個已存在的代理對象調(diào)用 reactive() 會返回其本身:
// 在同一個對象上調(diào)用 reactive() 會返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一個代理上調(diào)用 reactive() 會返回它自己
console.log(reactive(proxy) === proxy) // true依靠深層響應行,響應式對象內(nèi)的嵌套屬性依然是代理對象
const raw = {
name: '南玖'
}
const obj = {}
const person = reactive(raw)
person.hobby = obj
console.log('hobby', person.hobby) // Proxy(Object) {}
console.log('hobby === obj', person.hobby === obj) // falseshallowReactive
與shallowRef類似,shallowReactive也是用于聲明一個淺層的響應式對象,用于性能優(yōu)化處理
const shallowObj = shallowReactive({
name: '南玖',
age: 20,
info: {
hobby: 'run'
}
})
const changeNameProxy = () => {
shallowObj.info.hobby = 'swim' // 修改嵌套對象的屬性, 頁面不會更新
console.log('修改后的代理對象', shallowObj)
}
但如果同時修改頂層屬性與嵌套屬性的話,頁面也是會同時更新頂層值與嵌套值的渲染,一般來說我們要避免這樣使用,這會讓數(shù)據(jù)流難以理解和調(diào)試
const changeNameProxy = () => {
shallowObj.name = '小鹿' // 修改對象的頂層屬性
shallowObj.info.hobby = 'swim' // 修改嵌套對象的屬性
console.log('修改后的代理對象', shallowObj)
}
readonly
?
接受一個對象 (不論是響應式還是普通的) 或是一個 ref,返回一個原值的只讀代理。常用于數(shù)據(jù)保護
const shallowObjReadonly = readonly(shallowObj) // 創(chuàng)建只讀的淺響應式對象
shallowObjReadonly.name = 'nanjiu' // 只讀對象不能修改屬性, 會拋出錯誤總結
特性 | ref | reactive |
接受類型 | 任意類型 | 僅對象類型 |
訪問方式 | 通過.value訪問 | 直接訪問屬性 |
模板解包 | 自動解包(無需.value) | 無需解包 |
深層響應 | 默認支持 | 默認支持 |
性能優(yōu)化 | shallowRef | shallowReactive |
watch | 對于引用類型,watch默認不會開啟深度監(jiān)聽 | 默認開啟深度監(jiān)聽 |
引用替換 | 保持響應(.value=新引用) | 完全丟失響應 |
解構處理 | 需保持.value引用 | 需配合toRefs |
適用場景 | 基本類型、組件模板引用、跨函數(shù)傳遞 | 復雜對象、狀態(tài)管理、局部狀態(tài) |


































