Vue3 组合式API

什么是组合式 API?

使用 (datacomputedmethodswatch) 组件选项来组织逻辑通常都很有效。

然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。

这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的。

组合式 API 基础

setup 组件选项

新的 setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。

TIP

setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。

setup 选项是一个接收 propscontext 的函数。

此外,我们将 setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

让我们把 setup 添加到组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/components/UserRepositories.vue

export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
console.log(props) // { user: '' }

return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}

ref 的响应式变量

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:

1
2
3
import { ref } from 'vue'

const counter = ref(0)

ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:

1
2
3
4
5
6
7
8
9
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 中,NumberString 等基本类型是通过值而非引用传递的:

按引用传递与按值传递

在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

TIP

换句话说,ref 为我们的值创建了一个响应式引用。在整个组合式 API 中会经常使用引用的概念。

setup 内注册生命周期钩子

为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup 中注册生命周期钩子的方法。

组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on:即 mounted 看起来会像 onMounted

这些函数接受一个回调,当钩子被组件调用时,该回调将被执行。

watch 响应式更改

就像我们在组件中使用 watch 选项并在 user property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。它接受 3 个参数:

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项
1
2
3
4
5
6
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,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5' 记录到控制台中。

以下是等效的选项式 API:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}

有关 watch 的详细信息,请参阅我们的深入指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'

// 在我们组件中
setup (props) {
// 使用 `toRefs` 创建对 `props` 中的 `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 的变化做出反应。

独立的 computed 属性

refwatch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。

1
2
3
4
5
6
7
8
import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读响应式引用

为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

对于其他的逻辑关注点我们也可以这样做,但是你可能已经在问这个问题了——这不就是把代码移到 setup 选项并使它变得非常大吗?嗯,确实是这样的。这就是为什么我们要在继续其他任务之前,我们首先要将上述代码提取到一个独立的组合式函数中。

Setup

参数

使用 setup 函数时,它将接收两个参数:

  • `props``
  • ``context`

Props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

1
2
3
4
5
6
7
8
9
10
// MyBook.vue

export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}

TIP

props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

1
const { createApp, defineAsyncComponent } = Vue
1
2
3
4
5
6
7
8
9
// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
const { title } = toRefs(props)

console.log(title.value)
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

1
2
3
4
5
6
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}

Context

context 是一个普通 JavaScript 对象,暴露了其它可能在 setup 中有用的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MyBook.vue

export default {
setup(props, context) {
// Attribute (非响应式对象,等同于 $attrs)
console.log(context.attrs)

// 插槽 (非响应式对象,等同于 $slots)
console.log(context.slots)

// 触发事件 (方法,等同于 $emit)
console.log(context.emit)

// 暴露公共 property (函数)
console.log(context.expose)
}
}

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

1
2
3
4
5
6
// MyBook.vue
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。

attrsslots 的 property 是响应式的。如果你打算根据 attrsslots 的更改应用副作用,那么应该在 onBeforeUpdate 生命周期钩子中执行此操作。

访问组件的 property

执行 setup 时,你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods
  • refs (模板 ref)

结合模板使用

如果 setup 返回一个对象,那么该对象的 property 以及传递给 setupprops 参数中的 property 就都可以在模板中访问到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 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 返回的 refs 在模板中访问时是被自动浅解包的,因此不应在模板中使用 .value

使用渲染函数

setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:

1
2
3
4
5
6
7
8
9
10
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])
}
}

返回一个渲染函数将阻止我们返回任何其它的东西。从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。

我们可以通过调用 expose 来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value

expose({
increment
})

return () => h('div', count.value)
}
}

increment 方法现在将可以通过父组件的模板 ref 访问。

使用 this

setup() 内部,this 不是该活跃实例的引用

setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

选项式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

1
2
3
4
5
6
7
8
9
10
// MyBook.vue

export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}

Provide / Inject

我们也可以在组合式 API 中使用 provide/inject。两者都只能在当前活动实例的 setup() 期间调用。

设想场景