前言
上一章,我们学习了组合式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 基本使用
reactive是vue3提供的用于创建响应式数据的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>
多次点击修改按钮, 结果如下 
通过示例的运行结果, 我们有如下总结:
- 无论是
reactive函数创建响应对象,还是普通对象数据, 初始都会渲染视图 reactive函数返回的响应对象,在修改数据时,视图会自动更新- 创建普通对象在修改数据时, 视图不会发生变化
但需要注意的是, 当你多次点击普通对象修改时, 页面不会发生变化, 如果你接着点击修改响应数据, 你会发现页面渲染的普通对象数据也发生了. 原因在于视图更新,所有的数据都会数据都会用最新数据.
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 有两条限制:
有限值类型: 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。不能整体替换对象: 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地"替换"一个响应式对象,因为这将导致对初始引用的响应性连接丢失:解构操作不友好: 当我们解构响应式对象中属性值, 赋值给变量, 或作为实参传入函数, 会丢失响应性连接
局限性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 使用总结:
- reactive() 函数接受一个对象类型的参数, 返回一个具有响应性的代理对象
- reactive() 函数参数是一个嵌套的深层对象时, 返回的代理对象也具有深层响应性
- reactive() 函数如果参数对象的属性值时
ref数据, 在操作此属性时,会自动对`ref``数据解包 - reactive() 函数如果参数数组的某项是
ref数据时, 在通过下标获取此项ref数据操作时,不会解包 - 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>
示例中, 因为user 是ref数据, 在操作user.value 时会触发响应性, 修改视图, 因为我们在change修改数据时, 可以整体替换.value 值对应的对象
2.3. ref API 使用总结:
- ref() 参数如果是基本数据类型时, 返回一个对象,对象的
.value操作基本数据类型值 - ref() 参数如果是一个对象,那么这个对象会自动被
reactive包裹处理为响应对象, - ref() 参数如果是一个对象,对象中某个属性值时
ref数据,在操作此属性时,会自动解包 - ref() 参数如果是一个对象,
ref数据可以通过.value整体替换这个对象, 依然保持响应性 - 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源码分析专栏