Why is Composition API ?

创建 Vue 组件允许我们将界面的可重复部分及其功能提取到可重用的代码段中。 仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。 然而,我们的集体经验证明,仅凭这一点可能还不够,尤其是当您的应用程序变得非常大时——想想几百个组件。 在处理如此大的应用程序时,共享和重用代码变得尤为重要。

假设在我们的应用程序中,我们有一个视图来显示某个用户的存储库列表。 最重要的是,我们希望应用搜索和过滤功能。 我们处理此视图的组件可能如下所示:

// src/components/UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // using `this.user` to fetch user repositories
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

这个组件有几个职责:

  • 从该用户名的假定外部 API 获取存储库,并在用户更改时刷新它
  • 使用 searchQuery 字符串搜索存储库
  • 使用过滤器对象过滤存储库

使用组件选项(数据、计算、方法、监视)组织逻辑在大多数情况下都有效。 然而,当我们的组件变大时,逻辑问题的列表也会增加。 这会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写它们的人。

example

示例展示了一个大型组件,其中的逻辑关注点按颜色分组。

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

如果我们可以搭配与相同逻辑关注点相关的代码,那就更好了。 而这正是 Composition API 使我们能够做到的

Basics of Composition API

现在我们知道了原因,我们可以了解如何做。 要开始使用 Composition API,我们首先需要一个可以实际使用它的地方。 在 Vue 组件中,我们称这个地方为 setup。

setup Component Option

新的设置组件选项在组件创建之前执行,一旦 props 被解析,并作为组合 API 的入口点。

WARNING

您应该避免在 setup 内部使用它,因为它不会引用组件实例。 setup 在解析数据属性、计算属性或方法之前调用,因此它们在 setup 中不可用。

setup 选项应该是一个接受道具和上下文的函数,我们将在后面讨论。 此外,我们从 setup 返回的所有内容都将暴露给我们组件的其余部分(计算属性、方法、生命周期钩子等)以及组件的模板。

让我们在组件中添加设置:

// src/components/UserRepositories.vue

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

    return {} // anything returned here will be available for the rest of the component
  }
  // the "rest" of the component
}

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

  1. 从假定的外部 API 获取该用户名的存储库并在用户更改时刷新它

我们将从最显而易见的部分开始:

  • 储存库清单
  • 更新仓库列表的函数
  • 同时返回列表和函数,以便其他组件选项可以访问它们
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'

// inside our component
setup (props) {
  let repositories = []
  const getUserRepositories = async () => {
    repositories = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories // functions returned behave the same as methods
  }
}

这是我们的起点,但它还没有工作,因为我们的 repositories 变量不是响应式的。 这意味着从用户的角度来看,存储库列表将保持为空。 让我们解决这个问题!

响应式变量 ref

在 Vue 3.0 中,我们可以使用新的 ref 函数使任何变量在任何地方都具有反应性,如下所示

import { ref } from 'vue'

const counter = ref(0)

ref 接受参数并将其返回包装在具有 value 属性的对象中,然后可用于访问或改变反应变量的值:

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 这样的原始类型是按值传递的,而不是按引用传递的:

pass by reference

在任何值周围都有一个包装对象允许我们安全地将它传递到我们的整个应用程序中,而不必担心在此过程中的某处丢失其反应性。

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

回到我们的示例,让我们创建一个响应式存储库变量:

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

// in our component
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories
  }
}

现在,每当我们调用 getUserRepositories 时,存储库都会发生变化,并且视图将更新。 我们的组件现在应该是这样的:

// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

export 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 方法中,很好地相互靠近。 剩下的就是在挂载的钩子中调用 getUserRepositories 并设置一个观察者来在用户属性发生变化时执行此操作。

生命周期钩子注册 内部 setup

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

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

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

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'

// in our component
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  onMounted(getUserRepositories) // on `mounted` call `getUserRepositories`

  return {
    repositories,
    getUserRepositories
  }
}

现在我们需要对用户道具所做的更改做出反应。 为此,我们将使用独立监视功能。

watch

就像我们如何使用 watch 选项在组件内的 user 属性上设置观察者一样,我们可以使用从 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.value = 5,watch 将触发并执行回调(第二个参数),在这种情况下,它将在我们的控制台中记录“新的计数器值是:5”。

下面是等价的 Options 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) {
  // using `toRefs` to create a Reactive Reference to the `user` property of `props`
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // update `props.user` to `user.value` to access the Reference value
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // set a watcher on the Reactive Reference to user prop
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}

您可能已经注意到在我们设置的顶部使用了 toRefs。 这是为了确保我们的观察者会对用户道具所做的更改做出反应。

有了这些更改,我们刚刚将整个第一个逻辑问题移到了一个地方。 我们现在可以对第二个关注点做同样的事情——基于 searchQuery 进行过滤,这次使用计算属性。

computed

与 ref 和 watch 类似,计算属性也可以使用从 Vue 导入的计算函数在 Vue 组件之外创建。 让我们回到我们的反例:

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

在这里,计算函数返回一个只读的响应式引用,该引用指向作为第一个参数传递给计算的类 getter 回调的输出。 为了访问新创建的计算变量的值,我们需要像使用 ref 一样使用 .value 属性。

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

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

// in our component
setup (props) {
  // using `toRefs` to create a Reactive Reference to the `user` property of props
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // update `props.user` to `user.value` to access the Reference value
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // set a watcher on the Reactive Reference to 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
  }
}

我们可以对其他逻辑问题做同样的事情,但您可能已经在问这个问题——这不只是将代码移到设置选项并使其变得非常大吗? 嗯,这是真的。 这就是为什么在继续执行其他职责之前,我们将首先将上述代码提取到一个独立的组合函数中。 让我们从创建 useUserRepositories 开始:

// src/composables/useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(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 {
      // Since we don’t really care about the unfiltered repositories
      // we can expose the filtered results under the `repositories` name
      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 {
      // Since we don’t really care about the unfiltered repositories
      // we can expose the end results under the `repositories` name
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

本文只是对composition API 的源文档的粗略解读,也在学习之中,欢迎小伙伴一起讨论。

posted @ 2022-01-01 12:14  只做你的向日葵  阅读(29)  评论(0编辑  收藏  举报