Vue 3 组合式 API 快速入门

组合式 API 提供了一种新的组织代码的方式:将逻辑相关的代码集中起来,而不是分散在各个组件选项中。setup() 方法就是代码的集散地。

组合式 API 的起点

setup() 方法

setup() 方法是 Vue 3 新增的生命周期钩子,它被调用的时机在 beforeMount 钩子之前,它和组件选项 data 一样应当返回一个对象,这个对象会被解构,以便在模板中使用。

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<p @click="age++">Age: {{ age }}</p>
</div>

<script>
Vue.createApp({
setup() {
const age = 14
return { age }
}
}).mount('#app')
</script>

不过,仅仅传递一个数字,神奇的事情不会发生。换句话说,点击可以修改 age 变量的值,但视图不会发生任何变化。age 只是个原始类型的变量。

获得响应式

ref() 方法

对于原始类型的值,必须调用 ref() 方法将它包装成一个对象。原始类型的值就储存在对象的 value 属性中。

1
2
3
4
5
setup() {
const age = Vue.ref(14)
console.log(age.value) // 14
return { age }
}

虽然 age 变量的最终目标仍然是用来储存一个数字,但 age 变量并不直接储存这个数字,而是储存了一个包装它的对象。这个对象可以被传递到其他地方。通过 value 属性可以修改它储存的值,并且所有用到它的地方都会同步更新。

Vue 3 将 ref() 返回的对象称为 引用引用对象

模板会自动使用引用对象的 value 属性,无需使用 age.value 的形式。

1
<p @click="age++">Age: {{ age }}</p>

reactive() 方法

原始类型用 ref() 处理,对象类型则用reactive() 处理。reactive() 是用代理对象包装普通对象,从而得到「响应式对象类型」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<p>Name: {{ state.name }}</p>
<p @click="user.age++">Age: {{ state.age }}</p>
<p>Hobby: {{ state.hobby.toString() }}</p>
</div>

<script>
Vue.createApp({
setup() {
const state = Vue.reactive({ name: 'John', age: 14, city: 'Boston' })
return { state }
}
}).mount('#app')
</script>

当引用对象成为响应式对象的属性时,也无需通过 value 属性访问。

1
2
3
let age = Vue.ref(14)
let state = Vue.reactive({ age }) // 只在响应式对象中成立
console.log(state.age) // 14

创建属性的引用

toRef() 方法

在视图中访问一个对象的属性未免有些繁琐。能否直接传递对象的属性呢?

对于对象类型的属性,这样做可行,因为 reactive() 是递归处理对象的。但是对于原始类型的属性,这样做就行不通了。这相当于用属性的初始值创建了一个原始类型的变量,一切又回到了原点。

点击可以修改 age 变量,但是这个 age 变量和 state 对象的 age 属性没有关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div id="app">
<p>Name: {{ name }}</p>
<p @click="age++">Age: {{ age }}</p>
<p @click="hobby.pop()">Hobby: {{ hobby.toString() }}</p>
</div>

<script>
Vue.createApp({
setup() {
const state = Vue.reactive({
name: 'John',
age: 14,
city: 'Boston',
hobby: ['reading', 'climbing', 'music']
})
return {
name: state.name,
age: state.age,
hobby: state.hobby
}
}
}).mount('#app')
</script>

要传递对象的属性,应该先创建该属性的引用,然后传递这个引用。这就需要借助 toRef() 方法。例如 toRef(obj, prop) 表示创建对 obj[prop] 的引用。

1
2
3
4
5
6
7
8
setup() {
...
return {
name: Vue.toRef(state, "name"),
age: Vue.toRef(state, "age"),
hobby: Vue.toRef(state, "hobby")
}
}

现在就可以在视图中访问这些属性了。

ref()toRef() 方法都用来创建引用对象,并且都用于处理原始类型,但 ref() 针对的是字面量,而 toRef() 针对的是现有对象的属性。

toRefs() 方法

对每个属性使用 toRef() 方法显然也很繁琐,尤其是在有很多个属性需要传递的时候。

好在 Vue 3 已经考虑到这点并提供了 toRefs() 方法。有了这个方法,上面 return 语句后面的一大串代码统统不用写。

1
2
3
4
setup() {
...
return Vue.toRefs(state)
}

获取被代理的对象

toRaw() 方法

toRaw() 方法用来获取代理对象包装的对象,相当于 reactive() 的逆操作。

1
2
3
4
5
6
7
const state = {
name: 'John',
age: 14,
city: 'Boston'
}
const stateRef = Vue.reactive(state)
console.log(Vue.toRaw(stateRef) === state) // true

注册生命周期钩子

在选项式 API 中,注册生命周期钩子是通过添加相应组件选项实现的,而在组合式 API 中则是通过调用相应方法实现的。例如,要注册 mounted 生命周期钩子,就调用 onMounted() 方法。

1
2
3
setup() {
Vue.onMounted(() => console.log('mounted'))
}

添加侦听器

watch() 方法

watch() 方法用来添加侦听器,以监控指定变量,并在变量发生改变时调用指定函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<p @click="age++">Age: {{ age }}</p>
</div>

<script>
Vue.createApp({
setup() {
const age = Vue.ref(14)
Vue.watch(age, (oldVal, newVal) => {
console.log(`The new value is ${newVal}`)
})
return { age }
}
}).mount('#app')
</script>

创建计算属性

computed() 方法

创建计算属性要用 computed() 方法,它返回一个只读引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<p @click="age++">Age: {{ age }}</p>
<p>Access Allowed: {{ accessAllowed }}</p>
</div>

<script>
Vue.createApp({
setup() {
const age = Vue.ref(14)
const accessAllowed = Vue.computed(() => age.value < 18 ? false : true)
return { age, accessAllowed }
}
}).mount('#app')
</script>

点击,直到 age 大于等于 18 时,accessAllowed 变为 true

可组合函数

将代码集中起来并非把所有代码都放到 setup() 方法中。组合式 API 都是一些全局方法,这意味着实际上在哪里使用它们都一样,只要最后把引用都集中到 setup() 方法中即可:

  1. 逻辑相关的代码放在同一个函数,称为可组合函数
  2. 可组合函数总是返回一个对象,以便向外传递引用
  3. setup() 方法中通过解构赋值取得每个可组合函数返回的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getUser() {
const state = Vue.reactive({
name: 'John',
age: 14,
city: 'Boston'
})
const accessAllowed = Vue.computed(() => state.age < 18 ? false : true)
return { ...Vue.toRefs(state), accessAllowed }
}

Vue.createApp({
setup() {
const { name, age, city, accessAllowed } = getUser()
return { name, age, city, accessAllowed }
}
}).mount('#app')

单文件组件

单文件组件是将组件的 JS、HTML 和 CSS 写在同一个文件中的组件。

1
2
3
4
5
6
7
8
9
10
11
<script>
/* JS */
</script>

<template>
<!-- HTML -->
</template>

<style>
/* CSS */
</style>

单文件组件中的 JS 部分必须是一个 ES 模块,用 import 语句导入方法,用 export 语句导出组件选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import { ref } from 'vue'
export default {
setup() {
const age = ref(14)
return { age }
}
}
</script>

<template>
<p @click="age++">Age: {{ age }}</p>
</template>

由于组合式 API 只会用到 setup() 一个组件选项,因此 Vue 3 提供了一种只需书写 setup() 函数体的简化形式。

1
2
3
4
5
6
7
8
<script setup>
import { ref } from 'vue'
const age = ref(14)
</script>

<template>
<p @click="age++">Age: {{ age }}</p>
</template>

放置在 <script setup> 标签中的代码最终会成为 setup() 的函数体,并且无需 return 语句,Vue 3 会自动将其中定义的变量导出。