前言
Vue3 提供了两种创建响应式数据的方式:ref
和 reactive
。它们有什么区别?在开发中该如何选择?本文将详细讲解它们的用法、适用场景,并介绍相关的辅助 API,如:
shallowRef
和shallowReactive
(浅层响应式)triggerRef
(手动触发 DOM 更新)customRef
(自定义响应式逻辑)readonly
(防止数据被修改)
读完本文,你将彻底理解 Vue3 的响应式系统,并能在项目中正确使用这些 API!
ref
ref
接受任意类型值,返回响应式对象,通过.value
访问
需要注意的是被ref
包装之后需要.value
来进行取值或赋值,模版除外
比如:
js
<template>
<!-- 无需.value -->
<p>{{ name }}</p>
</template>
<script setup lang="ts">
const name = ref('南玖')
// 需要.value
name.value = 'nanjiu'
</script>
接收任意值
ref可以接收基本类型、引用类型的数据以及DOM的ref的属性值
js
const name = ref('南玖')
const obj = ref({
name: '南玖',
age: 20
})
console.log(name)
console.log(obj)

- 如果ref接收的是一个基本类型的数据,那么
.value
保存的就是就是该原始值 - 如果ref接收的是一个引用类型的数据,那么
.value
保存的就是代理了该引用数据的proxy对象 - 无论是基本数据类型还是引用数据类型,最终返回的都是由 RefImpl 类构造出来的对象
响应式
ref
默认提供深层响应式,也就是说即使我们修改嵌套的引用类型数据,vue也能够检测到并触发页面更新
vue
<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都能够监听到数据的变化,说到监听数据变化,这就得提一下watch
方法了,虽然vue
能够监听到嵌套数据的变化,但是watch
函数如果监听的是ref
定义的引用类型数据,默认是不会开启深度监听的
js
<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
是无法监听到数据变化的,想要监听到这一变化,我们需要手动开启深度监听
js
watch(() => person.value, (newValue, oldValue) => {
console.log('person changed from', oldValue, 'to', newValue)
}, {
deep: true // 深度监听
})

shallowRef
由于ref
默认是深层响应式,但有时候我们为了性能考虑,也可以通过 shallowRef 来放弃深层响应性。对于浅层 ref,只有 .value
的访问会被追踪。
vue
<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>

修改属性值,虽然数据变化了,但是页面并不会更新,并且无法通过watch监听数据变化。
⚠️这里还有一点需要注意的是,ref与shallowRef最好不要一起使用,否则shallowRef会被影响
比如:
js
const animalAgeAdd = () => {
// 修改深响应式对象的属性
person.value.info.age++
// 修改浅响应式对象的属性
animal.value.age++
// 这样会导致页面上的animal.age 也会更新
}

triggerRef
强制触发依赖于一个浅层 ref的副作用,这通常在对浅引用的内部值进行深度变更后使用。
当一个浅层ref的属性值发生改变又想触发页面更新时,可以手动调用triggerRef来实现
js
const animal = shallowRef({
name: '小鹿',
age: 5
})
const animalAgeAdd = () => {
// 修改浅响应式对象的属性
animal.value.age++
triggerRef(animal) // 手动触发更新
}
customRef
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
customRef()
接收一个工厂函数作为参数,该函数接收 track
和 trigger
两个函数作为参数,并返回一个带有 get
和 set
方法的对象。
- track :用于收集依赖项。在
get
方法中调用,收集该 ref 所依赖的响应式数据。 - trigger :用于触发更新。在
set
方法中调用,通知依赖项更新视图。
js
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
实现一个自带防抖的响应式数据
js
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用于将一个引用类型数据声明为响应式数据,返回的是一个Proxy对象。
只接受引用类型数据
js
const car = reactive({
brand: 'GTR',
model: 'Corolla',
year: 2020,
info: {
color: 'red',
mileage: 15000
}
})
const carNum = reactive(100)
console.log('引用数据类型', car)
console.log('基本数据类型', carNum)

重要限制 :reactive
只接受对象类型,基本类型会原样返回并产生警告
从上图我们还能看到,正常使用的reactive
返回的是一个Proxy
对象,也就是说reactive 实现响应式就是基于ES6 Proxy 实现的。
响应式
与ref
一样,reactive
默认也是深层响应式,并且watch
的监听是默认开启深度监听的
js
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)
})

会丢失响应式的几个操作
- 对象引用发生变化
由于 Vue 的响应式跟踪是通过属性访问实现的,因此必须始终保持对响应式对象的相同引用。
js
let person = reactive({
name: 'nanjiu'
})
// 重新赋值
person = {
name: '南玖22',
}
// 这里再修改数据,页面并不会更新
const changeNameProxy = () => {
person.name = '小鹿' // 修改代理对象的属性
console.log('修改代理对象后', person) // Proxy(Object) {name: '小鹿'}
}

- 解构
当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,也将丢失响应性
js
let person = reactive({
name: 'nanjiu'
})
let { name } = person
const changeNameProxy = () => {
name = '小鹿' // 修改解构后的属性,页面不会更新,person.name也不会更新
console.log('修改代理对象后', person) // Proxy(Object) {name: 'nanjiu'}
}
原始对象与代理对象
reactive()
返回的是一个原始对象的 Proxy代理对象,两者是不相等的
js
const raw = {
name: '南玖'
}
const person = reactive(raw)
console.log('原始对象', raw)
console.log('响应式对象', person)
console.log('person === raw', person === raw) // false

- 原始对象与代理对象是相互影响的
js
const raw = {
name: '南玖'
}
const person = reactive(raw)
raw.name = '小鹿' // 修改原始对象的属性
// person.name = '小鹿' // 修改响应式对象的属性
console.log('原始对象', raw) // {name: '小鹿'}
console.log('响应式对象', person) // Proxy(Object) {name: '小鹿'}
当原始对象里面的数据发生改变时,代理对象的数据也会发生变化;当代理对象里面的数据发生变化时,对应的原始数据也会发生变化
既然两者可以相互影响,那么修改原始对象会不会触发页面更新呢?🤔
答案是不会的,只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
代理一致性
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
js
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
依靠深层响应行,响应式对象内的嵌套属性依然是代理对象
js
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) // false
shallowReactive
与shallowRef类似,shallowReactive也是用于声明一个浅层的响应式对象,用于性能优化处理
js
const shallowObj = shallowReactive({
name: '南玖',
age: 20,
info: {
hobby: 'run'
}
})
const changeNameProxy = () => {
shallowObj.info.hobby = 'swim' // 修改嵌套对象的属性, 页面不会更新
console.log('修改后的代理对象', shallowObj)
}

但如果同时修改顶层属性与嵌套属性的话,页面也是会同时更新顶层值与嵌套值的渲染,一般来说我们要避免这样使用,这会让数据流难以理解和调试
js
const changeNameProxy = () => {
shallowObj.name = '小鹿' // 修改对象的顶层属性
shallowObj.info.hobby = 'swim' // 修改嵌套对象的属性
console.log('修改后的代理对象', shallowObj)
}

readonly
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。常用于数据保护
js
const shallowObjReadonly = readonly(shallowObj) // 创建只读的浅响应式对象
shallowObjReadonly.name = 'nanjiu' // 只读对象不能修改属性, 会抛出错误
总结
特性 | ref | reactive |
---|---|---|
接受类型 | 任意类型 | 仅对象类型 |
访问方式 | 通过.value访问 | 直接访问属性 |
模板解包 | 自动解包(无需.value) | 无需解包 |
深层响应 | 默认支持 | 默认支持 |
性能优化 | shallowRef | shallowReactive |
watch | 对于引用类型,watch默认不会开启深度监听 | 默认开启深度监听 |
引用替换 | 保持响应(.value=新引用) | 完全丢失响应 |
解构处理 | 需保持.value引用 | 需配合toRefs |
适用场景 | 基本类型、组件模板引用、跨函数传递 | 复杂对象、状态管理、局部状态 |