第四节:全局理解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源码分析专栏

相关推荐
逐·風1 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫2 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦3 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子3 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山3 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享4 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
程序媛小果4 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
从兄5 小时前
vue 使用docx-preview 预览替换文档内的特定变量
javascript·vue.js·ecmascript
凉辰5 小时前
设计模式 策略模式 场景Vue (技术提升)
vue.js·设计模式·策略模式
清灵xmf6 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询