前言
上一章,我们学习了组合式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
自动调用reactive
API, 将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
创建的数据.ref
API 我们后面会分析到, 先简单理解, 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
对于以上的使用场景, 推荐使用ref
API
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
源码分析专栏