组合式API
参考资料
介绍
什么是组合式API
通过创建Vue组件,我们可以将界面中重复的部分连同其功能一起提取为可复用的代码段。但是仅仅如此还不够,尤其当应用变得非常大的时候,共享和重用代码变得尤其重要。
假设我们的应用中有一个显示某个用户的仓库列表的视图。此外,我们还希望有搜索和筛选功能。实现此视图组件的代码可能如下所示:
export default{
components:{RepositoriesFilters,RepositoriesSortBy,RepositoriesList},
props:{
user:{
type:String,
required:true
}
},
data(){
return{
repositories:[],
filters:{},
searchQuery:''
}
},
computed:{
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch:{
user: 'getUserRepositories' // 1
},
methods:{
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted(){
this.getUserRepositories()// 1
}
}
该组件有以下几个职责:
- 从假定的外部API获取该用户的仓库,并在用户有任何更改时进行刷新
- 使用
searchQuey
字符串搜索仓库 - 使用
filters
对象筛选仓库
使用(data,computed,methods,watch)
组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长并逐渐碎片化。导致代码难以阅读、理解和维护。
此时,选项式的代码会暴露出这些问题,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式API使我们能够做到的。
组合式API基础
使用组合式API,首先需要一个可以实际使用它的地方。在Vue组件中,我们将此位置称为setup
。
setup组件选项
新的setup
选项在组件创建之前执行,一旦props
被解析,就会作为组合式API的入口。
注意
在setup
中应该避免使用this
,因为它不会找到组件实例。
setup
选项是一个接收props
和context
的函数,我们将setup
返回的所有内容暴露给组件的其余部分(计算属性,方法,生命周期钩子等)以及组件的模板。
让我们把setup
添加到组件中:
export default{
comoponents:{RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props:{
user: {
type: String,
required: true
}
},
setup(props){
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
},
//其余部分
}
现在让我们从提取第一个逻辑关注点开始(在原始代码段中标记为“1”)
1.从假定的外部API获取该用户的仓库,并在用户有任何更改时进行刷新
我们将从最明显的部分开始:
- 仓库列表
- 更新仓库列表的函数
- 返回列表和函数,以便其他组件可以对它们进行访问
// src/components/UserRepositories.vue `setup` function
import {fetchUserRepositories} from '@/api/repositories'
//在我们的组件内
setup(props){
let repositories=[]
const getUserRepositories=async()=>{
repositories=await fetchUserRepositories(props.user)
}
return {
repositoires,
getUserRepositories// 返回的函数与方法的行为相同
}
}
这是我们的出发点,但它还无法生效,因为repositories
变量是非响应式的。这意味着从用户的角度来看,仓库列表将始终为空。这个问题在下面解决。
带ref的响应式变量
在Vue3.0中,我们可以通过一个新的ref函数使任何响应式变量在任何地方起作用,如下所示:
import {ref} from 'vue'
const counter=ref(0)
ref
接收参数并将其包裹在一个带有value
property的对象中返回,然后可以使用该property访问或更改响应式变量的值:
import {ref} from 'vue'
const counter=ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
将值封装在一个对象中,看似没有必要,但为了保持JavaScript中不同数据类型的行为统一,这是必须的。这是因为在JavaScript中,Number或String等基本类型是通过值而非引用传递的
在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不用担心在某个地方失去它的响应性。
换句话说,ref
为我们的值创建了一个响应式引用。在整个组合式API中经常会使用引用的概念。
回到我们的例子,让我们创建一个响应式的repositories
变量:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
//在我们的组件中
setup(props){
const repositories=ref([])
const getUserRepositories=async()=>{
repositories.value=await fetchUserRepositories(props.user)
}
return{
repositories,
getUserRepositories
}
}
现在,每当我们调用getUserRepositories
时,repositories
都将发生变化,视图也会更新以反映变化。我们的组件现在应该如下所示:
// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
exprot default{
components:{RepositoriesFilters, RepositoriesSortBy, RepositoriesList},
props:{
user: {
type: String,
required: true
}
},
setup(props){
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
},
data(){
return {
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed:{
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch:{
user: 'getUserRepositories' // 1
},
methods:{
updateFilters () { ... }, // 3
},
mounted(){
this.getUserRepositories() // 1
}
}
我们已经将第一个逻辑关注点中的几部分转移到了setup
方法中,它们彼此非常接近。剩下的就是在mounted
钩子中调用getUserRepositories
,并设置一个监听器,以便在user Prop发生变化时执行此操作。
我们将从生命周期钩子开始。
在setup内注册生命周期钩子
为了使组合式API的功能和选项式API一样完整,我们还需要一种在setup
中注册声明周期钩子的方法。这要归功于Vue导出的几个新函数。组合式API上的生命周期钩子与选项式API的名称相同,但前缀为on
,即mounted
对应onMounted
。
这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// 在我们的组件中
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
现在我们需要对user
prop的变化做出反应。为此,我们将使用独立的watch
函数。
watch响应式更改
就像我们在组件中使用watch
选项并在user
property上设置侦听器一样,我们也可以从Vue导入的watch
函数执行相同的操作。它接受3个参数:
- 一个想要侦听的响应式引用 或getter函数
- 一个回调
- 可选的配置选项
它可以如下工作
import {ref,watch} from 'vue'
const counter=ref(0)
watch(counter,(newValue,oldValue)=>{
console.log('The new counter value is:'+counter.value)
})
每当counter
被修改时,例如counter.value=5
,侦听将触发并执行回调(第二个参数)。
以下是等效的选项式API:
export default{
data(){
return{
counter:0
}
},
watch:{
counter(newValue,oldValue){
console.log('The new counter value is: ' + this.counter)
}
}
}
现在将其应用到我们的示例中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// 在我们组件中
setup (props) {
// 使用 `toRefs` 创建对prop的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `prop.user` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
注意,在setup
顶部我们使用了toRefs
。这是为了确保我们的侦听器能够根据user
Prop的变化做出反应。
有了这些变化,我们就把第一个逻辑关注点转移到了一个地方。现在可以对第二个关注点执行相同的操作----基于searchQuery
进行过滤,这次是使用计算属性。
独立的computed属性
与ref
和watch
类似,也可以使用从Vue导入的computed
函数在Vue组件外部创建计算属性。回到counter
的例子:
import{ ref,computed} from 'vue'
const counter=ref(0)
const twiceTheCounter=computed(()=>counter.value*2)
counter.value++
console.log(counter.value)
console.log(twiceTheCounter.value)
这里我们给computed
函数传递了第一个参数,它是一个类似getter的回调函数,输出的是一个只读的响应式引用 。为了访问新创建的计算变量的value,我们需要像ref
一样使用.value
property
让我们将搜索功能移动到setup
中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'
// 在我们的组件中
setup (props) {
// 使用 `toRefs` 创建对 props 中的 `user` property 的响应式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `props.user ` 到 `user.value` 访问引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在 user prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value)
)
})
return {
repositories,
getUserRepositories,
searchQuery,
repositoriesMatchingSearchQuery
}
对于其他的逻辑关注点我们也可以这样做,但是这样就存在一个问题,setup
选项变得非常大。针对这个问题,我们可以将上述代码提取到一个独立的组合式函数中。让我们从创建useUserRepositories
函数开始:
// src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function userUserRepositories(user){
const repositories=ref([])
const getUserRepositories=async()=>{
repositories.value=await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user,getUserRepositories)
return{
repositories,
getUserRepositories
}
}
然后是搜索功能:
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了。以下是如何做到这一点:
// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'
export default{
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup (props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在 `repositories` 名称下暴露过滤后的结果
repositories: repositoriesMatchingSearchQuery,
getUserRepositories,
searchQuery,
}
},
data () {
return {
filters: { ... }, // 3
}
},
computed: {
filteredRepositories () { ... }, // 3
},
methods: {
updateFilters () { ... }, // 3
}
}
剩余的过滤功能依然如此。
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在 `repositories` 名称下暴露过滤后的结果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
Setup
基于组合式API和响应性原理
参数
使用setup
函数时,它将接收两个参数:
- props
- context
Props
setup
函数中的第一个参数是props
。setup
函数中的props
是响应式的,当传入新的prop时,它将被更新。
注意
因为props是响应式的,所以不能使用ES6解构,否则会消除prop的响应性。
如果需要解构prop,可以在setup
函数中使用toRefs
函数来完成此操作:
import {toRefs} from 'vue'
setup(props){
const {title}=toRefs(props)
console.log(title.value)
}
如果title
是可选的prop,则传入的props
中可能没有title
。在这种情况下,toRefs
不会为title
创建一个ref。这时需要用toRef
:
import {toRef} from 'vue'
setup(props){
const title=toRef(props,'title')
console.log(title.value)
}
Context
传递给setup
函数的第二个参数context
是一个普通的JavaScript对象,它暴露组件的三个property:
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
}
}
context
不是响应式的,这意味着可以安全地对其进行ES6解构
export default{
setup(props,{attrs,slots,emit}){
...
}
}
attrs
和slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该尽量避免对他们进行解构,并始终以attrs.x
或slots.x
的方式引用property。请注意,与props
不同,attrs
和slots
是非响应式的。如果打算根据它们更改副作用,那么应该在onUpdated
生命周期钩子中执行此操作。
访问组件的property
执行setup
时,组件实例尚未被创建。因此,只能访问以下property:
- props
- attrs
- slots
- emit
换句话说,你无法访问以下组件选项:
- data
- computed
- methods
结合模板使用
如果setup
返回一个对象,那么该对象的property以及传递给setup
的props
参数中的property就可以在模板中访问到:
<!-- MyBook.vue -->
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 暴露给 template
return {
readersNumber,
book
}
}
}
</script>
注意,从setup
返回的ref是被自动浅解包的,因此不影在模板中使用.value
来取值。
使用渲染函数
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
// MyBook.vue
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 请注意这里我们需要显式调用 ref 的 value
return () => h('div', [readersNumber.value, book.title])
}
}
使用this
在setup()
内部,this
不是该活跃实例的引用,因为setup()
是在解析其它组件选项之前被调用的。
Provide/Inject
你可以在组合式API中使用provide/inject
。两者都只能在当前活动实例的setup()
期间调用。
设想场景
假设要重新以下代码,其中包含一个MyMap
组件,该组件使用组合式API为MyMarker
组件提供用户的位置。
<!--MyMap.vue-->
<template>
<MyMarker/>
</template>
<script>
import MyMarker from 'MyMap.vue'
export default{
components:{
MyMarker
},
provide:{
location:'North Pole',
geolocation:{
longitude:90,
latitude:135
}
}
}
</script>
<!--MyMarker.vue-->
<script>
exprot default{
inject:['location','geolocation']
}
</script>
使用Provide
在setup()
中使用provide
时,我们首先从vue
显式导入provide
方法。
provide
函数允许通过两个参数定义property:
- name(
String
类型) - value
使用MyMap
组件后,provide的值可以按照如下方式重构:
<!--MyMap.vue-->
<template>
<MyMarker />
</template>
<script>
import {provide} from 'vue'
import MyMarker from 'MyMarker.vue'
export default{
components:{
MyMarker
},
setup(){
provide('location','North Pole')
provide('geolocation',{
longitude:90,
latitude:135
})
}
}
</script>
使用inject
在setup()
中使用inject,也需要从vue显式导入。导入以后,就可以调用它来定义暴露给组件的方式。
inject
函数有两个参数:
- 要inject的属性名
- 默认值(可选)
使用MyMarker组件,可以使用以下代码对其进行重构:
<!--MyMarker.vue-->
<script>
import {inject} from 'vue'
export default{
setup(){
const userLocation=inject('location','The Universe')
const userGeolocation=inject('geolocation')
return{
userLocation,
userGeolocation
}
}
}
</script>
响应性
添加响应性
为了增加provide和inject值之间的响应性,我们可以在provide值时使用ref
和reactive
。
使用MyMap
组件,我们的代码可以修改如下:
<!--MyMap.vue-->
<template>
<MyMarker />
</template>
<script>
import {provide,reactive,ref} from 'vue'
import MyMarker from 'MyMarker.vue'
export default {
components:{
MyMarker
},
setup(){
const location=ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
provide('location', location)
provide('geolocation', geolocation)
}
}
</script>
修改响应式property
当使用响应式provide/inject
值时,建议尽可能将对响应式property的所有修改限制在定义provide的组件内部。
然而,有时需要在注入数据的组件内部更新inject的数据。在这种情况下,建议provide一个方法来负责改变响应式property。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', location)
provide('geolocation', geolocation)
provide('updateLocation', updateLocation)
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
最后,如果要确保通过provide
传递的数据不会被inject的组件更改,建议对提供者的property使用readonly
。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
const updateLocation = () => {
location.value = 'South Pole'
}
provide('location', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}
</script>
模板引用
在使用组合式API时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明ref并从setup返回:
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import {ref,onMounted} from 'vue'
export default{
setup(){
const root=ref(null)
onMounted(()=>{
//DOM元素将在初始渲染后分配给ref
console.log(root.value)//<div>This is a root element</div>
})
return {root}
}
}
</script>
这里我们在渲染上下文中暴露root
,并通过ref="root"
,将其绑定到div作为其ref。在虚拟DOM补丁算法中,如果VNode的ref
键对应于渲染上下文中的ref,则VNode的相应元素或组件实例将被分配给该ref的值。这是在虚拟DOM挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
作为模板使用的ref的行为与其他ref一样:它们是响应式的,可以传递到(或从中返回)复合函数中。
JSX中的语法
export default {
setup() {
const root = ref(null)
return () =>
h('div', {
ref: root
})
// with JSX
return () => <div ref={root} />
}
}
v-for中的语法
组合式API模板引用在v-for
内部使用时没有特殊处理。相反,请使用函数引用自定义处理:
<template>
<div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
{{ item }}
</div>
</template>
<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// 确保在每次更新之前重置ref
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
侦听模板引用
侦听模板引用的变更可以替代生命周期钩子。
但与生命周期钩子的一个关键区别是,watch()
和watchEffect()
在DOM挂载或更新 之前 运行副作用,所以当侦听器运行时,模板引用还未被更新。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
// 这个副作用在 DOM 更新之前运行,因此,模板引用还没有持有对元素的引用。
console.log(root.value) // => null
})
return {
root
}
}
}
</script>
因此,使用模板引用的侦听器应该用flush:'post'
选项来定义,这将在DOM更新 后 运行副作用,确保模板引用与DOM保持同步,并引用正确的元素。
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, watchEffect } from 'vue'
export default {
setup() {
const root = ref(null)
watchEffect(() => {
console.log(root.value) // => <div>This is a root element</div>
},
{
flush: 'post'
})
return {
root
}
}
}
</script>