第四节:全局理解vue3中reactive, ref, readonly 三个响应式核心API的使用

前言

上一章,我们学习了组合式API的入口setup钩子函数. 那么本章就带着大家进入组合式API的学习.

通过vue官网查看组合式API , 相对来说还是比较多的

本章主要针对vue3组合式API 中响应式核心reactive, ref,Readonly三个API的讲解

1. reactive

vue2中定义在data选项中的数据会自动被vue处理为响应式数据, 所谓的响应式数据, 就是被vue给检测的数据, 当数据发生变化时, vue会自动处理一些副作用, 比如页面重新渲染.

vue3中, 不推荐使用data选项, 使用setup钩子函数, 在setup函数中, 如果需要使用响应式数据, 就需要我们通过API进行创建.

1.1. reactive 基本使用

reactivevue3提供的用于创建响应式数据的API, 本质就是一个函数, 我们看一下具体的函数类型

ts 复制代码
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

通过vue提供的reactive 函数的TypeScript类型, 可以看到,reactive 接受一个对象作为参数, 返回一个这个对象的响应式代理

因此我们可以使用 reactive() 函数创建一个响应式对象或数组:

创建响应数据

ts 复制代码
// 创建响应对象
const obj = reactive({ count:10 })

// 创建响应数组
const arr = reactive([10,20])

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户1(reactive创建的响应对象): {{ user }}</div>
    <div>用户2(普通对象): {{ person }}</div>
    <button @click="change">修改响应数据</button>
    <button @click="changeObj">修改普通对象数据</button>
  </div>
</template>

<script lang="ts">
import { defineComponent,  reactive } from 'vue';

export default defineComponent({
  setup(props, ctx) {
    // 通过reactive 创建一个响应对象user
    const user = reactive({ name: '张三', age: 18 })
    // 修改响应数据
    const change = () => {
      user.name = '张三22'

    }


    // 创建一个普通对象
    const person = { name: '李四' }
    // 修改普通对象数据
    const changeObj = () => {
      person.name = '李四222'
    }
    return { user, person, change, changeObj }

  }
})
</script>

多次点击修改按钮, 结果如下

通过示例的运行结果, 我们有如下总结:

  1. 无论是reactive 函数创建响应对象,还是普通对象数据, 初始都会渲染视图
  2. reactive 函数返回的响应对象,在修改数据时,视图会自动更新
  3. 创建普通对象在修改数据时, 视图不会发生变化

但需要注意的是, 当你多次点击普通对象修改时, 页面不会发生变化, 如果你接着点击修改响应数据, 你会发现页面渲染的普通对象数据也发生了. 原因在于视图更新,所有的数据都会数据都会用最新数据.

1.2. reactive 创建的是深层响应

reactive 创建的响应对象是深层响应, 所有嵌套的对象都会自动使用reactive 包裹创建为响应对象

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户: {{ user }}</div>
    <button @click="change">修改响应数据</button>
   
  </div>
</template>

<script lang="ts">
import { defineComponent,  reactive } from 'vue';

export default defineComponent({
  setup(props, ctx) {
    // 通过reactive 创建一个响应对象user
    const user = reactive({
      name: '张三',
      age: 18,
      friends: {
        name: '小明',
        age: 10
      }
    })
    console.log('friends',user.friends)
    
    // 修改响应数据
    const change = () => {
      user.friends.name = '小红'
    }

    return { user, change, }

  }
})
</script>

示例中: reactive 代理的对象是一个嵌套对象, 对象具有一个friends属性, friends 属性值也是一个对象.

此时当我们在控制台输出user.friends时, 我们看到输出的friends属性值 也是返回一个代理对象, 其源码内部, vue自动调用reactiveAPI, 将friends嵌套对象创建为响应式对象

因此当我们修改user.friends.name 属性值时, 视图也同样会发生变化

如果需要创建只有顶层具有响应性, 深层不具有响应性的响应对象,可以选择使用shallowReactive,此API 稍后再进行分析.

1.3. reactive 对于ref数据会自动解包

一个reactive 创建响应式对象会深层地解包任何 ref 属性,同时保持响应性。

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户: {{ user }}</div>
    <div>计数: {{ user.count }}</div>
    <button @click="change">修改响应数据</button>
   
  </div>
</template>

<script lang="ts">
import { defineComponent,  reactive, ref } from 'vue';

export default defineComponent({
  setup(props, ctx) {
    // 创建一个ref 数据
    const count = ref(10)

    
    // 通过reactive 创建一个响应对象user
    const user = reactive({
      name: '张三',
      age: 18,
     	count
    })

    
    console.log('count',user.count)
    // count 10
    
    // 修改响应数据
    const change = () => {
      user.count = 50
    }

    return { user, change, }

  }
})
</script>

在示例中, reactive 创建的user响应对象, 该对象具有一个count属性, 属性值是通过ref创建的数据.refAPI 我们后面会分析到, 先简单理解, ref创建响应式数据, 需要通过.value来获取值.

但是在这里我们会发现, 当我们通过user.count 获取ref数据时, 并不需要手动添加.value, 也能获取到ref数据的值, 因为vue会帮我们自动解包, 即自动获取.value的值

1.4. reactive 创建数组响应不会自动解包ref

值得注意的是,当我们通过reactive 创建响应数组或者Map, 这样的元素集合类型值, 集合中的ref 数据不会自动解包 , 需要通过.value获取ref数据

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>集合: {{ arr }}</div>
    <button @click="change">修改响应数据</button>
   
  </div>
</template>

<script lang="ts">
import { defineComponent,  reactive, ref } from 'vue';

export default defineComponent({
  setup(props, ctx) {
    // 创建一个ref 数据
    const count = ref(10)

    // 通过reactive 创建一个响应数组
    const arr = reactive([count])

    // 修改响应数据
    const change = () => {
      // 注意通过arr[0]获取的是ref数据, 不会自动解包
      arr[0].value = 50
    }

    return { arr, change, }

  }
})
</script>

通过示例的运行结果, 我们就会发现, 通过reactive 创建的数组响应, 数组项中如果存在ref数据, 在操作时,不会自动解包, 需要通过.value操作ref数据值

比如示例中通过arr[0]获取的是ref 数据, 没有自动解包, 因此在操作数据时,需要添加.value 赋值

1.5. reactive 局限性

官方对于reactive局限性的描述:

reactive() API 有两条限制:

  1. 有限值类型: 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
  2. 不能整体替换对象: 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地"替换"一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
  3. 解构操作不友好: 当我们解构响应式对象中属性值, 赋值给变量, 或作为实参传入函数, 会丢失响应性连接

局限性1:有限值类型

第一个局限的主要原因在于reactive 是通过Proxy 创建代理对象, 来实现数据的响应性.

但是Proxy 只能代理对象类型的数据,比如对象, 数组 和 Map, 和Set这样集合类型,

Proxy对于原始数据类型, 比如string,number,boolean 是无效.

因此reactive 无法创建基本数据类型响应性

ts 复制代码
const num = reactive(10)
const str = reactive('aa')
const bool = reactive(true)
const und = reactive(undefined)
const nul = reactive(null)

以上写法控制台全部会报警告, reactive的参数不能是原始数据类型

局限性2: 不能整体替换

reactive 第二个局限性主要原因在于js 引用数据类型的特性, 我们通过reactive创建代理对象赋值给一个变量, 变量中保存的是代理对象的内存地址, 调用代理对象属性实现响应性

如果在此期间,重新给这个变量赋值, 那么此变量保存的值将不在是原代理对象, 通过变量的操作将会失去响应性

示例

ts 复制代码
let user = reactive({ name: 10 })

user = {name:20}

示例中,我们先用reactive 创建了一个代理对象, 赋值给变量user, 此时user 是响应数据, 通过user 操作时会触发响应性

但是后续我们重新给user变量赋值了一个普通对象, 此时user 就失去了原有代理对象的引用, 此时操作user就是在操作一个不具有响应性的普通对象, 也就是我们常说的丢失响应性

这也是推荐使用const关键字声明响应式常量的原因.

局限性3: 解构操作不友好

当我们将响应式对象的属性 通过解构的方式赋值给本地变量 或传递给函数时,我们将丢失响应性连接

主要原因在于,响应性是通过代理对象操作属性时检测的. 如果我们只是将值获取出来赋值给一个变量, 这其实就是一个值的拷贝. 通过变量操作时, 是不会触发源数据的变化的. 此时变量时不具有响应性的.

这等同于创建了一个变量, 赋值一个基本类型数据. 是不具有响应性的.

示例:

ts 复制代码
const user = reactive({ name: 10 })

let { name } = use;

// 等同于
let name = 10

对于以上的使用场景, 推荐使用refAPI

1.6. reactive API 使用总结:

  1. reactive() 函数接受一个对象类型的参数, 返回一个具有响应性的代理对象
  2. reactive() 函数参数是一个嵌套的深层对象时, 返回的代理对象也具有深层响应性
  3. reactive() 函数如果参数对象的属性值时ref 数据, 在操作此属性时,会自动对`ref``数据解包
  4. reactive() 函数如果参数数组的某项是ref数据时, 在通过下标获取此项ref数据操作时,不会解包
  5. reactive() 函数的局限性为: (
    • 参数不能是基本类型数据,
    • reactive()赋值变量尽量不要整体替换,会丢失响应性
    • 对解构操作不友好, 解构操作会丢失响应性

2. ref

为什么需要ref创建响应性数据: 官方描述如下

reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有类型的 "引用" 机制。为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref

其实很好理解, 因为reactive函数 无法为基本数据类型的数据添加响应性, 因此创建了ref来处理基本数据类型的响应性.

注意: ref不仅可以创建基本数据类型的响应性, 也能创建对象的响应性. 如果ref是对象, 该对象会vue会自动调用reactive创建为响应式对象.

2.1. ref 基本使用

ref接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

示例:

html 复制代码
<template>
  <div class="user">
    <div>姓名:{{ name }}</div>
    <div>年龄{{ age }}</div>
    <button @click="change">修改信息</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "RefCom",
  setup() {
    // 用户信息
    let name = ref("张三");
    let age = ref(18);

    // 控制台输出name 值
    console.log("name", name);

    // 修改用户信息方法
    const change = () => {
      name.value = "李四";
      age.value = 20;
    };

    return {
      name,
      age,
      change,
    };
  },
});
</script>

控制台输出结果:

通过控制台输出结果, 你可以清晰的看到, ref数据只能通过value属性进行操作. 其他属性都是内部属性. 即vue处理ref数据时,自己操作的属性.

ref 的数据时可更改的, 通过.value属性赋予新值, 同时ref数据时具有响应性的, 操作时会触发响应, 进而更新视图

2.2. ref 的参数可以为对象

ref 的参数也可以是一个对象,如果参数是对象, 那么这个对象将会被vue自动通过reactive() 函数处理,转为具有深层响应式的对象, 这也意味着如果对象中嵌套ref数据, ref数据将会被深层解包

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户: {{ user }}</div>
    <button @click="change">修改响应数据</button>
  </div>
</template>

<script lang="ts">
import { defineComponent,  ref } from 'vue';

export default defineComponent({
  setup(props, ctx) {
  	// ref 参数为一个对象
    const user = ref({ name: '张三', age: 18,  })
    console.log('user', user)

    // 修改响应数据
    const change = () => {
      user.value.name = '李四'
    }
    return { user, change, }

  }
})
</script>

示例中, ref 的参数是一个对象, vue会自动的使用reactive()函数处理这个对象, 使其具有响应性.

因此当我们通过user.value.name修改数据时, 会触发响应性,修改视图

如果这个对象中某个属性值时ref数据, 那么在操作这个属性时,会自动解包

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户: {{ user }}</div>
    <button @click="change">修改响应数据</button>
  </div>
</template>

<script lang="ts">
import { defineComponent,  ref } from 'vue';

export default defineComponent({
  setup(props, ctx) {
    // ref 数据
    const sex = ref('男')
    
  	// ref 参数为一个对象
    const user = ref({ name: '张三', age: 18, sex })
    console.log('user', user)

    // 修改响应数据
    const change = () => {
      user.value.sex = '女'
    }
    return { user, change, }

  }
})
</script>

示例中, user.value.sex 获取到的是ref 数据, 但我们在change 方法中修改时,并不需要添加.value.

原因在于user.value 获取到的是{ name: '张三', age: 18, sex }通过reactive 处理后的代理对象,那么reactive 代理对象中属性值时ref数据, 操作时,ref数据会自动解包,

因此user.value.sex 不需要在添加.value 操作

如果ref 的参数是一个对象, ref数据操作时.value 是具有响应性的, 因为我们可以整体替换整个参数对象

示例:

html 复制代码
<template>
  <div class="">
    <h3>Reactivity</h3>
    <div>用户: {{ user }}</div>
    <button @click="change">修改响应数据</button>
  </div>
</template>

<script lang="ts">
import { defineComponent,  ref } from 'vue';

export default defineComponent({
  setup(props, ctx) {
  	// ref 参数为一个对象
    const user = ref({ name: '张三', age: 18,  })
    console.log('user', user)

    // 修改响应数据
    const change = () => {
      user.value = { name: '李四', age: 20 }
    }
    return { user, change, }

  }
})
</script>

示例中, 因为userref数据, 在操作user.value 时会触发响应性, 修改视图, 因为我们在change修改数据时, 可以整体替换.value 值对应的对象

2.3. ref API 使用总结:

  1. ref() 参数如果是基本数据类型时, 返回一个对象,对象的.value 操作基本数据类型值
  2. ref() 参数如果是一个对象,那么这个对象会自动被reactive包裹处理为响应对象,
  3. ref() 参数如果是一个对象,对象中某个属性值时ref数据,在操作此属性时,会自动解包
  4. ref() 参数如果是一个对象, ref 数据可以通过.value整体替换这个对象, 依然保持响应性
  5. ref() 数据在模板上会自动解包

3. reactive, ref类型标注

3.1. reactive 类型标注

正常情况下, 如果reactive()有参数, vue默认会通过参数推导类型

例如:

ts 复制代码
const user = reactive({ name: '张三', age: 18 })
/*
	此时推断user的类型为: 
 {
   name:string,
   age:number
 }
*/

因此,大多情况下我们并不需要给reactive 添加类型标注

如果推断类型和我们预期不符,那么我们就需要添加类型标注,

比如,在工作中,我们需要获取列表数据, 在定义接收数据时,会初始赋值空数组[], 此时类型推断有可能会推断为never[] 类型, 在后续赋值时就会带来问题

示例:

ts 复制代码
 interface TItem {
  name: string
  age: number
}

const list: TItem[] = reactive([])

list.push({
  name: '张三',
  age: 18
})

示例中,如果list 不添加类型注释 TItem[],list 会被推断为never[]类型, 后续push 一个对象时将会报错

所以此时我们需要给list 添加类型标注

添加类型标注也可以通过reactive 泛型添加

ts 复制代码
 interface TItem {
  name: string
  age: number
}

const list = reactive<TItem[]>([])

list.push({
  name: '张三',
  age: 18
})

3.2. ref 类型标注

ref 会根据初始化时的值推导其类型:

ts 复制代码
const name = ref('张三')
// 推断name 类型: Ref<string>
// 其中 string 是 name.value 的类型 

name.value = 10
// 报错: 不能将number 类型的值赋值给string 类型

但在工作中, 有时我们可能想为 ref 内的值指定一个更复杂的类型,可以通过使用 Ref 这个类型:

ts 复制代码
  const name: Ref<string | number> = ref('张三')
  // 此时name 类型: Ref<string | number
  // 其中name.value 值的类型:  string 和number 的联合类型

  name.value = 10
// 因此这里给name.value 赋值number 类型的值不会报错

或者,在调用 ref() 时传入一个泛型参数,来覆盖默认的推导行为

ts 复制代码
 const name = ref<string | number>('张三')

如果调用ref 但没有赋初始值, 默认推断为Ref<any>类型

ts 复制代码
const name = ref()
// 推断name 类型: Ref<any>

如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:

ts 复制代码
const name = ref<number>()
// 推断name 类型: Ref<number | undefined>

4. readonly

4.1. readonly 基本使用

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理

示例:

ts 复制代码
//1. readonly 参数为普通对象,
// 返回一个只读代理user
const user = readonly({ name: '小明', age: 8 })
console.log('user', user)

user.name = '小红'
// 报错:无法为"name"赋值,因为它是只读属性。


// 2. readonly 参数为代理对象,
const person = reactive({ name: '小明', age: 8 })
const user2 = readonly(person)
console.log('user2', user2)

user2.name = '小红'
// 报错:无法为"name"赋值,因为它是只读属性。

// 3. readonly 参数为ref,
const count = ref(10)
const user3 = readonly(count)
console.log('user3', user3)

user3.value = 10
// 报错:无法为"value"赋值,因为它是只读属性

示例中,无论readonly 参数是普通对象,还是代理对象, 亦或是ref 数据, 返回的都是只读代理, 只能获取值, 不能修改值

4.2. readonly 只读是 深层的

也就是说如果readonly 参数是一个深层对象, 那么对任何嵌套属性的访问都将是只读的

示例:

ts 复制代码
// readonly 是深层只读
const user = readonly({
  name: '小明',
  age: 8,
  friends: {
    name: '小红',
    sex: '女'
  }
})
console.log('user', user)

// 修改深层也是只读的
user.friends.name = '小红'
// 报错:无法为"name"赋值,因为它是只读属性。

4.3. readonly 对于ref会自动解包

readonly对于 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。

示例:

ts 复制代码
// readonly 对于ref 数据自动 解包
const count = ref(10)
const user = readonly({
  name: '小明',
  age: 8,
  count
})

// 获取ref 数据时会自动解包
console.log('count', user.count)

// ref 解包也是只读的, 修改报错
user.count = 20
// 报错:无法为"count"赋值,因为它是只读属性

如果要避免深层只读代理,请使用 shallowReadonly() API作替代。此API 分析

5. 结语

至此,就给大家讲完了vue3三个核心的响应式API. 其中reative, ref是工作中最常使用到的API. 希望大家可以认真学习,研究各种使用细节.

如果想了解这些API 内部实现的原理, 可以关注订阅vue3源码分析专栏

相关推荐
伍哥的传说26 分钟前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
yugi98783828 分钟前
前端跨域问题解决Access to XMLHttpRequest at xxx from has been blocked by CORS policy
前端
浪裡遊39 分钟前
Sass详解:功能特性、常用方法与最佳实践
开发语言·前端·javascript·css·vue.js·rust·sass
旧曲重听11 小时前
最快实现的前端灰度方案
前端·程序人生·状态模式
默默coding的程序猿2 小时前
3.前端和后端参数不一致,后端接不到数据的解决方案
java·前端·spring·ssm·springboot·idea·springcloud
夏梦春蝉2 小时前
ES6从入门到精通:常用知识点
前端·javascript·es6
归于尽2 小时前
useEffect玩转React Hooks生命周期
前端·react.js
G等你下课2 小时前
React useEffect 详解与运用
前端·react.js
我想说一句2 小时前
当饼干遇上代码:一场HTTP与Cookie的奇幻漂流 🍪🌊
前端·javascript
funnycoffee1232 小时前
Huawei 6730 Switch software upgrade example版本升级
java·前端·华为