Vue3初探(一)组合式API

Vue3发布也有一段时间了,虽然还在beta测试,但是有些特性也可以事先了解下,这次就先看一下最重要的几个特性之一的组合式API,一种类似React Hooks的东西。

如果你了解Vue2或者说看过我以前关于Vue2源码的分析,有一个结论我现在就可以抛出:

就是Vue3的组合式API本身并没有用什么本质上的新特性,还是响应式原理那一套东西,只不过是提供了一种新的语法可以让你将强相关的一些列的data,prop,compute,method,生命周期函数封装到一起,最后将需要暴露出去的data也好,compute也好通过一个对象return出去,剩下的就和vue2没什么区别,你可以直接使用this去调用或者使用。

文章主要内容来自于Vue3迁移指南-组合式API,中间会根据自己的思路重新整理并解释。

为什么需要组合式API

通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。

但是当我们的组件变得非常复杂的时候,我们在平时实际工作中很经常会遇到这种情况,我们需要watch一个prop的变化,然后改变某个data,而这个data有可能导致某个计算属性的改变,这其中可能还会涉及其他的计算属性和data,也可能会用到很多函数

如果只是一个prop引起的一系列变化还好,但一般情况下我们的组件不只有一个prop,每个prop的变化都会引起一系列函数调用的时候,所有的函数,计算属性等混在一起就会让人理不清头绪

如果我们有比较好的编码习惯,可以人为的给代码分块,也就是把功能相关的函数放在一起,比如文档中举的例子,相同颜色的函数是功能相关的:

但这样还是不够清晰,于是Vue3想了个办法,组合式API,提供了一种新的语法,可以让我们将功能相关的数据和函数放在一起。

Vue2的写法

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
26
27
28
29
30
31
// src/components/UserRepositories.vue

export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}

组合式API

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

新的 setup 组件选项在创建组件之前执行,一旦 props 被解析,并充当合成 API 的入口点。

由于在执行 setup 时尚未创建组件实例,因此在 setup 选项中没有 this。这意味着,除了 props 之外,你将无法访问组件中声明的任何属性——本地状态计算属性方法

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

我们先看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'

// 在我们的组件内
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}

return {
repositories,
getUserRepositories // 返回的函数与方法的行为相同
}
}

这个例子的setup中接受参数是props,利用prop中的数据去获取仓库数据,然后赋值给repositories。

这是我们的出发点,但它还不能工作,因为我们的 repositories 变量是非响应式的。这意味着从用户的角度来看,仓库列表将保持为空,因为vue的MVVM是基于响应式对象的,如果repositories是非响应式的,那就意味着它不会触发任何视图的更新。

ref 的响应式变量

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

1
2
3
import { ref } from 'vue'

const counter = ref(0)

ref 接受参数并返回它包装在具有 value property 的对象中,然后可以使用该 property 访问或更改响应式变量的值:

注意,ref返回的是个对象,入参是放到了返回对象的value中

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 等基本类型是通过值传递的,而不是通过引用传递的:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 时,repositories 都将发生变化,视图将更新以反映更改。我们的组件现在应该如下所示:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'

export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
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

这些函数接受在组件调用钩子时将执行的回调。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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
}
}

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

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,watch 将触发并执行回调 (第二个参数),在本例中,它将把 '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)
}
}
}

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

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` 创建对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)

// 在用户 prop 的响应式引用上设置一个侦听器
watch(user, getUserRepositories)

return {
repositories,
getUserRepositories
}
}

你可能已经注意到在我们的 setup 的顶部使用了 toRefs。这是为了确保我们的侦听器能够对 user prop 所做的更改做出反应。

独立的 computed 属性

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

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 函数返回一个作为 computed 的第一个参数传递的 getter 类回调的输出的一个只读响应式引用。为了访问新创建的计算变量的 value,我们需要像使用 ref 一样使用 .value property。

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

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
26
27
28
29
30
31
32
33
34
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

// in our component
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)

// 在用户 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 开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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
}
}

然后是搜索功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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
}
}

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

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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 }
},
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
}
}