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

前言

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

  • shallowRefshallowReactive(浅层响应式)
  • 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() 接收一个工厂函数作为参数,该函数接收 tracktrigger 两个函数作为参数,并返回一个带有 getset 方法的对象。

  • 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
适用场景 基本类型、组件模板引用、跨函数传递 复杂对象、状态管理、局部状态
相关推荐
一点也不想取名4 分钟前
解决 Java 与 JavaScript 之间特殊字符传递问题的终极方案
java·开发语言·javascript
[email protected]33 分钟前
Asp.Net Core SignalR导入数据
前端·后端·asp.net·.netcore
小满zs6 小时前
Zustand 第五章(订阅)
前端·react.js
涵信6 小时前
第一节 基础核心概念-TypeScript与JavaScript的核心区别
前端·javascript·typescript
谢尔登7 小时前
【React】常用的状态管理库比对
前端·spring·react.js
编程乐学(Arfan开发工程师)7 小时前
56、原生组件注入-原生注解与Spring方式注入
java·前端·后端·spring·tensorflow·bug·lua
小公主7 小时前
JavaScript 柯里化完全指南:闭包 + 手写 curry,一步步拆解原理
前端·javascript
姑苏洛言9 小时前
如何解决答题小程序大小超过2M的问题
前端
TGB-Earnest9 小时前
【leetcode-合并两个有序链表】
javascript·leetcode·链表
GISer_Jing9 小时前
JWT授权token前端存储策略
前端·javascript·面试