前言

Vue3 提供了两种创建响应式数据的方式:refreactive。它们有什么区别?在开发中该如何选择?本文将详细讲解它们的用法、适用场景,并介绍相关的辅助 API,如:

  • shallowRefshallowReactive(浅层响应式)
  • triggerRef(手动触发 DOM 更新)
  • customRef(自定义响应式逻辑)
  • readonly(防止数据被修改)

读完本文,你将彻底理解 Vue3 的响应式系统,并能在项目中正确使用这些 API!

ref

ref接受任意类型值,返回响应式对象,通过.value访问

需要注意的是被ref包装之后需要.value 来进行取值或赋值,模版除外

比如:

<template>
    <!-- 无需.value -->
    <p>{{ name }}</p>
</template>
<script setup lang="ts">
const name = ref('南玖')
// 需要.value
name.value = 'nanjiu'
</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

接收任意值

ref可以接收基本类型、引用类型的数据以及DOM的ref的属性值

const name = ref('南玖')
const obj = ref({
    name: '南玖',
    age: 20
})
console.log(name)
console.log(obj)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

Vue3响应式核心:ref vs reactive深度对比_数据

  • 如果ref接收的是一个基本类型的数据,那么.value保存的就是就是该原始值
  • 如果ref接收的是一个引用类型的数据,那么.value保存的就是代理了该引用数据的proxy对象
  • 无论是基本数据类型还是引用数据类型,最终返回的都是由 RefImpl 类构造出来的对象

响应式

ref默认提供深层响应式,也就是说即使我们修改嵌套的引用类型数据,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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

Vue3响应式核心:ref vs reactive深度对比_响应式_02

也就是说无论嵌套多深,vue都能够监听到数据的变化,说到监听数据变化,这就得提一下watch方法了,虽然vue能够监听到嵌套数据的变化,但是watch函数如果监听的是ref定义的引用类型数据,默认是不会开启深度监听的

<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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

Vue3响应式核心:ref vs reactive深度对比_JavaScript_03

虽然页面视图更新了,但是watch是无法监听到数据变化的,想要监听到这一变化,我们需要手动开启深度监听

watch(() => person.value, (newValue, oldValue) => {
    console.log('person changed from', oldValue, 'to', newValue)
}, {
    deep: true // 深度监听
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Vue3响应式核心:ref vs reactive深度对比_响应式_04

shallowRef

由于ref默认是深层响应式,但有时候我们为了性能考虑,也可以通过 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

Vue3响应式核心:ref vs reactive深度对比_数据_05

修改属性值,虽然数据变化了,但是页面并不会更新,并且无法通过watch监听数据变化。

⚠️这里还有一点需要注意的是,ref与shallowRef最好不要一起使用,否则shallowRef会被影响

比如:

const animalAgeAdd = () => {
    // 修改深响应式对象的属性
    person.value.info.age++
    // 修改浅响应式对象的属性
    animal.value.age++
    // 这样会导致页面上的animal.age 也会更新
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

Vue3响应式核心:ref vs reactive深度对比_数据_06

triggerRef

强制触发依赖于一个浅层 ref的副作用,这通常在对浅引用的内部值进行深度变更后使用。

当一个浅层ref的属性值发生改变又想触发页面更新时,可以手动调用triggerRef来实现

const animal = shallowRef({
    name: '小鹿',
    age: 5
})

const animalAgeAdd = () => {
    // 修改浅响应式对象的属性
    animal.value.age++

    triggerRef(animal) // 手动触发更新
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

customRef

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

customRef() 接收一个工厂函数作为参数,该函数接收 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

  • track:用于收集依赖项。在 get 方法中调用,收集该 ref 所依赖的响应式数据。
  • trigger:用于触发更新。在 set 方法中调用,通知依赖项更新视图。
const myRef = customRef((track, trigger) => {
    let value = 0
    return {
        get() {
            track()
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                value = newValue
                trigger()
            }
        }
    }
})
console.log(myRef)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

customRef允许我们通过获取或设置一个变量的值时进行一些额外的操作,而不需要侦听这个变量进行额外的操作。

比如,我们可以使用cusromRef实现一个自带防抖的响应式数据

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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

reactive

reactive用于将一个引用类型数据声明为响应式数据,返回的是一个Proxy对象。

只接受引用类型数据

const car = reactive({
    brand: 'GTR',
    model: 'Corolla',
    year: 2020,
    info: {
        color: 'red',
        mileage: 15000
    }
})
const carNum = reactive(100)

console.log('引用数据类型', car)
console.log('基本数据类型', carNum)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

Vue3响应式核心:ref vs reactive深度对比_前端_07

重要限制reactive只接受对象类型,基本类型会原样返回并产生警告

从上图我们还能看到,正常使用的reactive返回的是一个Proxy对象,也就是说reactive 实现响应式就是基于ES6 Proxy 实现的。

响应式

ref一样,reactive默认也是深层响应式,并且watch的监听是默认开启深度监听的

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)
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

Vue3响应式核心:ref vs reactive深度对比_数据_08

会丢失响应式的几个操作
  • 对象引用发生变化

由于 Vue 的响应式跟踪是通过属性访问实现的,因此必须始终保持对响应式对象的相同引用。

let person = reactive({
    name: 'nanjiu'
})
// 重新赋值
person = {
    name: '南玖22',
}
// 这里再修改数据,页面并不会更新
const changeNameProxy = () => {
    person.name = '小鹿' // 修改代理对象的属性
    console.log('修改代理对象后', person) // Proxy(Object) {name: '小鹿'}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

Vue3响应式核心:ref vs reactive深度对比_响应式_09

  • 解构

当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,也将丢失响应性

let person = reactive({
    name: 'nanjiu'
})
let { name } = person
const changeNameProxy = () => {
    name = '小鹿' // 修改解构后的属性,页面不会更新,person.name也不会更新
    console.log('修改代理对象后', person) // Proxy(Object) {name: 'nanjiu'}
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

原始对象与代理对象

  • reactive() 返回的是一个原始对象的 Proxy代理对象,两者是不相等的
const raw = {
    name: '南玖'
}
const person = reactive(raw)

console.log('原始对象', raw)
console.log('响应式对象', person)
console.log('person === raw', person === raw) // false
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Vue3响应式核心:ref vs reactive深度对比_Vue.js_10

  • 原始对象与代理对象是相互影响的
const raw = {
    name: '南玖'
}
const person = reactive(raw)

raw.name = '小鹿' // 修改原始对象的属性
// person.name = '小鹿' // 修改响应式对象的属性

console.log('原始对象', raw)  // {name: '小鹿'}
console.log('响应式对象', person) // Proxy(Object) {name: '小鹿'}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

当原始对象里面的数据发生改变时,代理对象的数据也会发生变化;当代理对象里面的数据发生变化时,对应的原始数据也会发生变化

既然两者可以相互影响,那么修改原始对象会不会触发页面更新呢?🤔

答案是不会的,只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本

代理一致性

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

依靠深层响应行,响应式对象内的嵌套属性依然是代理对象

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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

shallowReactive

与shallowRef类似,shallowReactive也是用于声明一个浅层的响应式对象,用于性能优化处理

const shallowObj = shallowReactive({
    name: '南玖',
    age: 20,
    info: {
        hobby: 'run'
    }
})

const changeNameProxy = () => {
    shallowObj.info.hobby = 'swim' // 修改嵌套对象的属性, 页面不会更新
    console.log('修改后的代理对象', shallowObj) 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

Vue3响应式核心:ref vs reactive深度对比_JavaScript_11

但如果同时修改顶层属性与嵌套属性的话,页面也是会同时更新顶层值与嵌套值的渲染,一般来说我们要避免这样使用,这会让数据流难以理解和调试

const changeNameProxy = () => {
    shallowObj.name = '小鹿' // 修改对象的顶层属性
    shallowObj.info.hobby = 'swim' // 修改嵌套对象的属性
    console.log('修改后的代理对象', shallowObj) 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Vue3响应式核心:ref vs reactive深度对比_JavaScript_12

readonly

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。常用于数据保护

const shallowObjReadonly = readonly(shallowObj) // 创建只读的浅响应式对象

shallowObjReadonly.name = 'nanjiu' // 只读对象不能修改属性, 会抛出错误
  • 1.
  • 2.
  • 3.

总结

特性

ref

reactive

接受类型

任意类型

仅对象类型

访问方式

通过.value访问

直接访问属性

模板解包

自动解包(无需.value)

无需解包

深层响应

默认支持

默认支持

性能优化

shallowRef

shallowReactive

watch

对于引用类型,watch默认不会开启深度监听

默认开启深度监听

引用替换

保持响应(.value=新引用)

完全丢失响应

解构处理

需保持.value引用

需配合toRefs

适用场景

基本类型、组件模板引用、跨函数传递

复杂对象、状态管理、局部状态