原文链接

什么是组合式 API?

通过创建 Vue 组件,我们可以将界面中重复的部分连同其功能一起提取为可重用的代码段。仅此一项就可以使我们的应用在可维护性和灵活性方面走得相当远。然而,我们的经验已经证明,光靠这一点可能并不够,尤其是当你的应用变得非常大的时候——想想几百个组件。处理这样的大型应用时,共享和重用代码变得尤为重要

假设我们的应用中有一个显示某个用户的仓库列表的视图。此外,我们还希望有搜索和筛选功能。实现此视图组件的代码可能如下所示:

  1. // src/components/UserRepositories.vue
  2. export default {
  3. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  4. props: {
  5. user: {
  6. type: String,
  7. required: true
  8. }
  9. },
  10. data () {
  11. return {
  12. repositories: [], // 1
  13. filters: { ... }, // 3
  14. searchQuery: '' // 2
  15. }
  16. },
  17. computed: {
  18. filteredRepositories () { ... }, // 3
  19. repositoriesMatchingSearchQuery () { ... }, // 2
  20. },
  21. watch: {
  22. user: 'getUserRepositories' // 1
  23. },
  24. methods: {
  25. getUserRepositories () {
  26. // 使用 `this.user` 获取用户仓库
  27. }, // 1
  28. updateFilters () { ... }, // 3
  29. },
  30. mounted () {
  31. this.getUserRepositories() // 1
  32. }
  33. }

该组件有以下几个职责:

  1. 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新
  2. 使用 searchQuery 字符串搜索仓库
  3. 使用 filters 对象筛选仓库

使用 (datacomputedmethodswatch) 组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。
options-api.png
这是一个大型组件的示例,其中逻辑关注点按颜色进行分组。

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

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

组合式 API 基础

既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup

setup 组件选项

新的 setup 选项在组件创建之前执行(注:取代了beforeCreatecreated),一旦 props 被解析,就将作为组合式 API 的入口。

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

setup 选项是一个接收 propscontext 的函数,我们将在之后进行讨论。此外,我们将 setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板

让我们把 setup 添加到组件中:

  1. // src/components/UserRepositories.vue
  2. export default {
  3. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  4. props: {
  5. user: {
  6. type: String,
  7. required: true
  8. }
  9. },
  10. setup(props) {
  11. console.log(props) // { user: '' }
  12. return {} // 这里返回的任何内容都可以用于组件的其余部分
  13. }
  14. // 组件的“其余部分”
  15. }

现在让我们从提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。

  1. 从假定的外部 API 获取该用户的仓库,并在用户有任何更改时进行刷新

我们将从最明显的部分开始:

  • 仓库列表
  • 更新仓库列表的函数
  • 返回列表和函数,以便其他组件选项可以对它们进行访问
  1. // src/components/UserRepositories.vue `setup` function
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. // 在我们的组件内
  4. setup (props) {
  5. let repositories = []
  6. const getUserRepositories = async () => {
  7. repositories = await fetchUserRepositories(props.user)
  8. }
  9. return {
  10. repositories,
  11. getUserRepositories // 返回的函数与方法的行为相同
  12. }
  13. }

这是我们的出发点,但它还无法生效,因为 repositories 变量是非响应式的(注:只有响应式的数据类型在模板中表现得才是响应式)。这意味着从用户的角度来看,仓库列表将始终为空。让我们来解决这个问题!

ref 的响应式变量

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

  1. import { ref } from 'vue'
  2. const counter = ref(0)

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

  1. import { ref } from 'vue'
  2. const counter = ref(0)
  3. console.log(counter) // { value: 0 }
  4. console.log(counter.value) // 0
  5. counter.value++
  6. console.log(counter.value) // 1

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,NumberString 等基本类型是通过值而非引用传递的:

介绍 - 图2

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

注:ref 为值创建了一个对象,并借助 Proxy 来监测对象改变,然后将该对象的引用返回给变量,此时该变量存放的则是一个响应式引用

  1. let a = 1
  2. let b = a
  3. a++; // a 发生变化,但 b 不会发生变化,而对象是按照引用来进行传递的。

衍生面试题 在 Vue3 中假设有个 count 响应式变量,为什么采用 count.value 来修改响应式变量值,而不是直接对count 进行修改?

答:避免失去响应式,保持不同数据类型的行为统一,借助 Proxy 可以监测到数据发生改变。针对 Number 或 String 等基本类型是通过值进行传递的,若不通过封装对象进行包裹,则会失去响应性。

回到我们的例子,让我们创建一个响应式的 repositories 变量:

  1. // src/components/UserRepositories.vue `setup` function
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref } from 'vue'
  4. // 在我们的组件中
  5. setup (props) {
  6. const repositories = ref([])
  7. const getUserRepositories = async () => {
  8. repositories.value = await fetchUserRepositories(props.user)
  9. }
  10. return {
  11. repositories,
  12. getUserRepositories
  13. }
  14. }

完成!现在,每当我们调用 getUserRepositories 时,repositories 都将发生变化,视图也会更新以反映变化。我们的组件现在应该如下所示:

  1. // src/components/UserRepositories.vue
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref } from 'vue'
  4. export default {
  5. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  6. props: {
  7. user: {
  8. type: String,
  9. required: true
  10. }
  11. },
  12. setup (props) {
  13. const repositories = ref([])
  14. const getUserRepositories = async () => {
  15. repositories.value = await fetchUserRepositories(props.user)
  16. }
  17. return {
  18. repositories,
  19. getUserRepositories
  20. }
  21. },
  22. data () {
  23. return {
  24. filters: { ... }, // 3
  25. searchQuery: '' // 2
  26. }
  27. },
  28. computed: {
  29. filteredRepositories () { ... }, // 3
  30. repositoriesMatchingSearchQuery () { ... }, // 2
  31. },
  32. watch: {
  33. user: 'getUserRepositories' // 1
  34. },
  35. methods: {
  36. updateFilters () { ... }, // 3
  37. },
  38. mounted () {
  39. this.getUserRepositories() // 1
  40. }
  41. }

我们已经将第一个逻辑关注点中的几个部分移到了 setup 方法中,它们彼此非常接近。剩下的就是在 mounted 钩子中调用 getUserRepositories,并设置一个监听器,以便在 user prop 发生变化时执行此操作。

我们将从生命周期钩子开始。

setup 内注册生命周期钩子

为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup 中注册生命周期钩子的方法。这要归功于 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 **on**:即 mounted 看起来会像 onMounted

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

让我们将其添加到 setup 函数中:

  1. // src/components/UserRepositories.vue `setup` function
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref, onMounted } from 'vue'
  4. // 在我们的组件中
  5. setup (props) {
  6. const repositories = ref([])
  7. const getUserRepositories = async () => {
  8. repositories.value = await fetchUserRepositories(props.user)
  9. }
  10. onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`
  11. return {
  12. repositories,
  13. getUserRepositories
  14. }
  15. }

现在我们需要对 user prop 的变化做出反应。为此,我们将使用独立的 watch 函数。

watch 响应式更改

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

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项

下面让我们快速了解一下它是如何工作的

  1. import { ref, watch } from 'vue'
  2. const counter = ref(0)
  3. watch(counter, (newValue, oldValue) => {
  4. console.log('The new counter value is: ' + counter.value)
  5. })

每当 counter 被修改时,例如 counter.value=5,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5' 记录到控制台中。

以下是等效的选项式 API:

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

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

现在我们将其应用到我们的示例中:

  1. // src/components/UserRepositories.vue `setup` function
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref, onMounted, watch, toRefs } from 'vue'
  4. // 在我们组件中
  5. setup (props) {
  6. // 使用 `toRefs` 创建对prop的 `user` property 的响应式引用
  7. const { user } = toRefs(props)
  8. const repositories = ref([])
  9. const getUserRepositories = async () => {
  10. // 更新 `prop.user` 到 `user.value` 访问引用值
  11. repositories.value = await fetchUserRepositories(user.value)
  12. }
  13. onMounted(getUserRepositories)
  14. // 在 user prop 的响应式引用上设置一个侦听器
  15. watch(user, getUserRepositories)
  16. return {
  17. repositories,
  18. getUserRepositories
  19. }
  20. }

你可能已经注意到在我们的 setup 的顶部使用了 **toRefs**。这是为了确保我们的侦听器能够根据 **user** prop 的变化做出反应

有了这些变化,我们就把第一个逻辑关注点移到了一个地方。我们现在可以对第二个关注点执行相同的操作——基于 searchQuery 进行过滤,这次是使用计算属性。

独立的 computed 属性

refwatch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。让我们回到 counter 的例子:

  1. import { ref, computed } from 'vue'
  2. const counter = ref(0)
  3. const twiceTheCounter = computed(() => counter.value * 2)
  4. counter.value++
  5. console.log(counter.value) // 1
  6. console.log(twiceTheCounter.value) // 2

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

让我们将搜索功能移到 setup 中:

  1. // src/components/UserRepositories.vue `setup` function
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref, onMounted, watch, toRefs, computed } from 'vue'
  4. // 在我们的组件中
  5. setup (props) {
  6. // 使用 `toRefs` 创建对 props 中的 `user` property 的响应式引用
  7. const { user } = toRefs(props)
  8. const repositories = ref([])
  9. const getUserRepositories = async () => {
  10. // 更新 `props.user ` 到 `user.value` 访问引用值
  11. repositories.value = await fetchUserRepositories(user.value)
  12. }
  13. onMounted(getUserRepositories)
  14. // 在 user prop 的响应式引用上设置一个侦听器
  15. watch(user, getUserRepositories)
  16. const searchQuery = ref('')
  17. const repositoriesMatchingSearchQuery = computed(() => {
  18. return repositories.value.filter(
  19. repository => repository.name.includes(searchQuery.value)
  20. )
  21. })
  22. return {
  23. repositories,
  24. getUserRepositories,
  25. searchQuery,
  26. repositoriesMatchingSearchQuery
  27. }
  28. }

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

  1. // src/composables/useUserRepositories.js
  2. import { fetchUserRepositories } from '@/api/repositories'
  3. import { ref, onMounted, watch } from 'vue'
  4. export default function useUserRepositories(user) {
  5. const repositories = ref([])
  6. const getUserRepositories = async () => {
  7. repositories.value = await fetchUserRepositories(user.value)
  8. }
  9. onMounted(getUserRepositories)
  10. watch(user, getUserRepositories)
  11. return {
  12. repositories,
  13. getUserRepositories
  14. }
  15. }

然后是搜索功能:

  1. // src/composables/useRepositoryNameSearch.js
  2. import { ref, computed } from 'vue'
  3. export default function useRepositoryNameSearch(repositories) {
  4. const searchQuery = ref('')
  5. const repositoriesMatchingSearchQuery = computed(() => {
  6. return repositories.value.filter(repository => {
  7. return repository.name.includes(searchQuery.value)
  8. })
  9. })
  10. return {
  11. searchQuery,
  12. repositoriesMatchingSearchQuery
  13. }
  14. }

现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了。以下是如何做到这一点:

  1. // src/components/UserRepositories.vue
  2. import useUserRepositories from '@/composables/useUserRepositories'
  3. import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
  4. import { toRefs } from 'vue'
  5. export default {
  6. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  7. props: {
  8. user: {
  9. type: String,
  10. required: true
  11. }
  12. },
  13. setup (props) {
  14. const { user } = toRefs(props)
  15. const { repositories, getUserRepositories } = useUserRepositories(user)
  16. const {
  17. searchQuery,
  18. repositoriesMatchingSearchQuery
  19. } = useRepositoryNameSearch(repositories)
  20. return {
  21. // 因为我们并不关心未经过滤的仓库
  22. // 我们可以在 `repositories` 名称下暴露过滤后的结果
  23. repositories: repositoriesMatchingSearchQuery,
  24. getUserRepositories,
  25. searchQuery,
  26. }
  27. },
  28. data () {
  29. return {
  30. filters: { ... }, // 3
  31. }
  32. },
  33. computed: {
  34. filteredRepositories () { ... }, // 3
  35. },
  36. methods: {
  37. updateFilters () { ... }, // 3
  38. }
  39. }

此时,你可能已经知道了其中的奥妙,所以让我们跳到最后,迁移剩余的过滤功能。我们不需要深入了解实现细节,因为这并不是本指南的重点。

  1. // src/components/UserRepositories.vue
  2. import { toRefs } from 'vue'
  3. import useUserRepositories from '@/composables/useUserRepositories'
  4. import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
  5. import useRepositoryFilters from '@/composables/useRepositoryFilters'
  6. export default {
  7. components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  8. props: {
  9. user: {
  10. type: String,
  11. required: true
  12. }
  13. },
  14. setup(props) {
  15. const { user } = toRefs(props)
  16. const { repositories, getUserRepositories } = useUserRepositories(user)
  17. const {
  18. searchQuery,
  19. repositoriesMatchingSearchQuery
  20. } = useRepositoryNameSearch(repositories)
  21. const {
  22. filters,
  23. updateFilters,
  24. filteredRepositories
  25. } = useRepositoryFilters(repositoriesMatchingSearchQuery)
  26. return {
  27. // 因为我们并不关心未经过滤的仓库
  28. // 我们可以在 `repositories` 名称下暴露过滤后的结果
  29. repositories: filteredRepositories,
  30. getUserRepositories,
  31. searchQuery,
  32. filters,
  33. updateFilters
  34. }
  35. }
  36. }

我们完成了!

请记住,我们只触及了组合式 API 的表面以及它允许我们做什么。要了解更多信息,请参阅深入指南。