组合式 API (Composition API) 是Vue3和Vue2的v2.7之后版本中的全新特性,是一系列API的的集合(响应式API、生命周期钩子、依赖注入等等),其风格是基于函数的组合,以一种更直观、更灵活的方式来书写Vue单文件组件。但是组合式 API并不属于函数式编程,因为函数式编程主要强调的是数据不可变,而以 Vue 中数据可变的、细粒度的响应性系统为基础的。
接下来我们讲述的相关知识点,都是基于Vue3的组合式API来进行的。
组合式API最大的优势在于可以通过组合函数的方式,将组件进行任意颗粒度的拆分和组合,这样就大大提高了代码的可维护性和复用性。
同时,组合式API可以将处理某一逻辑的所有相关代码,放置在相邻区域内,解决了选项式API中将处理统一逻辑的代码分散在不同的选项中,在查看时需要上下来回进行滚动切换的弊端,方便后期将相关逻辑抽出,封装为单独组件。官方示例图片如下,相同颜色的代码块,表示处理某一逻辑的相关代码:
组合式API主要利用基本的变量和函数,这些东西本身就是类型友好的,因此组合式API对于TypeScript的支持性更好。
使用组合式API()的单文件组件中的组件模板(
)在打包时,会被编译成一个内联函数,与JS代码位于同一作用域内,可以直接访问定义的变量和函数,无需像选项式API一样依赖
this
这个上下文对象来访问属性。由于变量名可以被压缩,但是对象的属性名不能被压缩,所以组合式API对于代码压缩更友好,打包后的体积也就更小。
setup()
钩子函数(不常用) 这种方法并不常用,通常只用于以下两种情况:① 在非单文件组件中使用组合式API时,例如:在html文件中引入Vue。② 在基于选项式API的单文件组件中集成使用组合式API时,例如:在Vue2中使用。
通过使用setup()
函数来定义响应式数据,然后将该函数返回的对象暴露给模板和组件实例,然后就可以在选项式API中通过this
来获取相关属性,如果在模板中使用变量,则无需使用this
。但是在setup()
函数中,是无法访问组件实例的,也就是this
对象。
<script>
// 引入响应式API
import { ref } from 'vue'
export default {
// 使用setup()钩子函数
setup() {
// 声明响应式变量
const count = ref(0)
// 返回值会暴露给模板和其他的选项式 API
return {
count
}
},
mounted() {
// 通过this来访问声明的响应式变量
console.log(this.count) // 0
}
}
script>
<template>
<button @click="count++">{{ count }}button>
template>
setup()
函数具有两个参数:第一个参数为props
,用于获取组件实例中接受的props
参数,会根据props参数的变化,响应式的变化;第二个参数为context
,表示一个Setup上下文对象,该对象暴露了一些常用的API对象:
export default {
// 组件接收父组件传递过来的props数据
props: {
title: String
},
// setup的第一个参数为props 第二个参数为context
setup(props, context) {
// 以props.xxx的形式方式访问数据 不要使用数据解构 这样会失去响应性
console.log(props.title)
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
}
}
更多详细用法请查阅:组合式 API:setup()。
(常用) 该方法是我们在Vue3中的主流用法,通过在单文件组件的标签中增加
setup
,标识该标签内部使用组合式API,大大简化了setup()
函数的繁琐语法。而且在中的顶层(非局部)中导入的模块、声明的变量和函数,可以在当前单文件组件中直接使用。
<script setup>
// 引入要使用的API
import { ref, onMounted } from 'vue'
// 声明变量
let count = ref(0);
// 声明函数
const addCount = () => {
// 在函数中修改变量 需要调用.value
count.value++;
}
// 在生命周期中使用声明的变量
onMounted(() => {
// 获取变量值需要调用.value
console.log('mounted---',count.value)
})
script>
<template>
<button @click="addCount">{{ count }}button>
template>
在组合式API中,推荐使用ref()
函数来声明响应式变量,该函数可以声明任何类型的值,包括String
、Number
等简单类型,也包括深层嵌套的对象、数组以及JS内置的Map、Set等内置数据结构。简单类型的变量可以ref()
可以直接声明,复杂类型的变量则会在内部直接调用 reactive()
函数来实现。
想要使用 ref()
函数,需要先从vue
对象中将该函数引入到当前组件中:
// 引入ref()函数API
import { ref } from 'vue'
ref()
函数接收一个参数,这个参数为声明的响应式变量的初始值,然后函数会返回一个带有.value
属性的ref对象。我们需要使用let/const/var
声明一个变量,来接收ref()
函数返回的响应式对象,至此,一个响应式变量就创建成功了。
// 通过ref()函数声明响应式变量
const count = ref(0)
通过ref()
函数声明的变量,如果是在的顶层(非局部)中声明的,那可以在整个单文件组件中访问。在组件的
标签中修改或访问时,需要通过
变量名.value
的形式访问,但是在组件的模板中使用变量,包括
{{ }}
模板语法、事件监听器的表达式等等情况,可以直接使用变量名
,此时响应式变量会自动解包,获取其value
属性。
<script setup>
// 省略前面的引入和声明
// 直接访问count 返回一个含有value属性的对象
console.log(count) // { value: 0 }
// 访问count.value
console.log(count.value) // 0
// 修改变量值
count.value++
// 再次访问变量
console.log(count.value) // 1
script>
<template>
<button @click="count++">{{ count }}button>
template>
在组件的模板中只有顶级的ref变量才会被自动解包,如果是非顶级的ref变量,则不会解包,例如在一个对象中将属性值声明为ref变量:
<script setup>
// 省略前面的引入和声明
// 顶级的ref变量
let obj3 = ref({ count: 0 });
// 非顶级的ref变量
let obj4 = { count: ref(0) };
script>
<p>{{ obj3.count }}p>
<p>{{ obj4.count }}p>
<p>{{ obj3.count + 1 }}p>
<p>{{ obj4.count + 1 }}p>
reactive()
函数是组合式API声明响应式变量的另外一种方式,只能用来声明复杂类型的数据变量(Object
、Array
、Map
、Set
等),不能声明简单类型的数据变量。想要使用 reactive()
函数,需要先从vue
对象中将该函数引入到当前组件中:
// 引入reactive()函数API
import { reactive } from 'vue'
reactive()
函数接收一个参数,表示声明的响应式变量的初始对象值,然后函数会返回一个原始数据对象的 Proxy
对象,该对象是响应式的。最终再使用let/const/var
声明一个变量,来接收reactive()
函数返回的响应式对象,至此,一个响应式变量就创建成功了:
// 通过reactive()函数声明响应式变量
const countObj = reactive({ count: 0 })
通过reactive()
函数声明的变量,如果是在的顶层(非局部)中声明的,那可以在整个单文件组件中访问。在组件的
标签中和在
模板中使用变量时,直接使用
变量名
即可:
<script setup>
// 省略前面的引入和声明
// 直接调用变量名 会返回一个代理对象
console.log(countObj); // Proxy(Object) {count: 0}
// 调用代理对象的属性
console.log(countObj.count); // 0
// 代理对象的属性值可以直接修改
countObj.count++;
console.log(countObj.count); // 1
script>
<template>
<button @click="state.count++">{{ state.count }}button>
template>
reactive()
返回的是原始对象的Proxy
代理对象,它和原始对象是不全等的,只有Proxy
对象是响应式的,更改原始对象不会触发更新。而且为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同一个的Proxy
对象,而对一个已存在的Proxy
对象调用 reactive()
会返回其本身。
// 创建一个原始对象
let obj = { count: 0 };
// 使用reactive()创建一个响应式对象
let countObj = reactive(obj);
// 代理对象和原始对象并不相等
console.log(obj === countObj); // false
// 在同一个原始对象上重复调用reactive()会返回相同的代理
console.log(countObj === reactive(obj)); // true
// 在代理对象上再次调用reactive()会返回其本身
console.log(countObj === reactive(countObj)); // true
如果reactive()
函数包裹的变量初始值为一个多层嵌套对象的数据,reactive()
会深层的转换代理对象,无论嵌套多少层,都会将每一层对象转换为代理对象,最终返回的Proxy
对象内部全部都是响应式的。
// 声明一个嵌套对象
let obj2 = { test: { count: 0 } };
// 使用reactive()创建一个响应式对象
let countObj2 = reactive(obj2);
对于一个通过reactive()
创建的响应式对象,如果我们使用对象解构,将其简单类型的属性解构为一个变量时,或者将简单类型的属性传递给函数作为参数时,变量和参数都会失去响应性的效果:
// 当解构时,count 已经与 countObj.count 断开响应式连接
let { count } = countObj
// 修改解构出的count的变量值 不会影响countObj的 count
count++
console.log(count) // 1
console.log(countObj.count) // 0
// 该函数接收到的是一个普通的数字 无法响应式的追踪到countObj.count的变化
testFunction(countObj.count) {
// ...
}
// 如果想要保持响应式的特性 则需要将整个响应式对象传入其中
testFunction2(countObj) {
// ...
}
如果通过reactive()
创建的响应式数组或原生集合类型(如 Map
) 中,使用ref()声明的变量作为元素,其不会被自动解包,需要通过.value
的形式访问:
// 响应式数组中使用ref()声明的变量作为元素
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
// 响应式集合中使用ref()声明的变量作为元素
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
shallowRef()
函数是ref()
函数的浅层作用形式,函数接收一个参数,这个参数为声明的响应式变量的初始值,但是函数不会返回一个Proxy
代理对象,返回的是原始数据本身。此时只有对变量.value
的访问是响应式的。如果想要转换为响应式的原始数据是复杂类型的数据变量,则更改其内部的属性值并不会有响应式,只有变更整个value
的值才会触发响应式。
如果原始数据是具有多层嵌套的复杂数据类型,也只有对.value
的访问是响应式的。因此shallowRef()
常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成,避免对大型数据的响应性开销。
// 引入 shallowRef() API
import { ref, shallowRef } from 'vue'
// 声明一个js对象
const obj = { num: 1 };
// 利用ref声明一个响应式变量
const state = ref(obj)
// 利用shallowRef声明一个浅层响应式变量
const state2 = shallowRef(obj)
// 输出两个变量
console.log(state.value); // 一个Proxy代理对象:Proxy(Object) {num: 1}
console.log(state2.value); // 一个普通对象:{num: 1}
// 判断响应式变量和原对象是否相等
console.log(state.value === obj); // false
// 判断浅层响应式变量和原对象是否相等
console.log(state2.value === obj); // true
// 判断浅层响应式变量和响应式变量是否相等
console.log(state2.value === state.value); // false
// 直接修改内部属性值不会触发响应式
state.value.count = 2;
// 直接修改value属性才会触发响应式
state.value = { count: 3 };
shallowRef()
函数返回的是原始数据本身,如果我们在组件被挂载之前修改浅层响应式变量的内部属性值,依旧会被响应式监听到。例如:在中声明浅层变量后,直接修改其内部属性值,或者在
onBeforeMount()
生命周期钩子函数中修改其内内部属性值。
<script setup>
// 引入相关API
import { onBeforeMount, onMounted, shallowRef } from 'vue'
// 声明一个js对象
const obj = { num: 1 };
// 利用shallowRef声明一个浅层响应式变量
const state2 = shallowRef(obj);
// 在组件挂载之前 修改变量的内部属性值 页面显示会更新
onBeforeMount(() => {
state2.value.num = 1111;
});
// 在组件挂载之后 修改变量的内部属性值 页面显示不会更新
onMounted(() => {
console.log(state2.value); // {num: 1111}
state2.value.num = 2222;
console.log(state2.value); // {num: 2222}
})
script>
<p>{{ state2.num }}p>
shallowReactive()
函数时reactive()
函数的浅层作用形式,通过该函数的声明的响应式对象变量,只有根对象中的属性才是响应式的,如果变量包含嵌套的对象结构,则嵌套对象的属性值不会被转换成响应式,只有整个替换属性值时才会触发响应式。
// 引入 shallowReactive API
import { shallowReactive } from 'vue'
// 声明一个原始对象
const obj = { num: 1, next: { count: 1 } };
// 声明一个浅层响应式对象
const state = shallowReactive(obj);
// 输出该对象
console.log(state); // Proxy(Object) {num: 1, next: {…}}
// 直接修改根属性 可以触发响应式
state.num = 2222;
// 直接修改嵌套属性 不会触发响应式
state.next.count = 2222;
// 重新赋值嵌套属性 可以触发响应式
state.next = { count: 3333 };
如果我们在组件被挂载之前修改其嵌套对象的属性值,依旧会被响应式监听到。例如:在中声明浅层对象变量后,直接修改其嵌套对象的属性值,或者在
onBeforeMount()
生命周期钩子函数中修改其嵌套对象的属性值。
<script setup>
// 引入相关API
import { onBeforeMount, onMounted, shallowRef } from 'vue'
// 声明一个原始对象
const obj = { num: 1, next: { count: 1 } };
// 声明一个浅层响应式对象
const state = shallowReactive(obj);
// 在组件挂载之前 修改变量的内部属性值 页面显示会更新
onBeforeMount(() => {
state.next.count = 2222;
});
// 在组件挂载之后 修改变量的内部属性值 页面显示不会更新
onMounted(() => {
console.log(state.next.count); // {num: 2222}
state.next.count = 3333;
console.log(state.next.count); // {num: 3333}
})
script>
<p>{{ state.next.count}}p>