Vue3的Composition API详解
Vue3之Composition API详解
Vue3 之 Composition API 详解
Composition API
也叫组合式 API,是 Vue3.x 的新特性。
通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要
通俗的讲:
没有Composition API
之前 vue 相关业务的代码需要配置到 option 的特定的区域,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高。Vue3.x 中的 composition-api 就是为了解决这个问题而生的
compositon api 提供了以下几个函数:
setup
ref
reactive
watchEffect
watch
computed
toRefs
- 生命周期的
hooks
一、setup 组件选项
新的
setup
组件选项在创建组件之前执行,一旦props
被解析,并充当合成API
的入口点
提示:
由于在执行
setup
时尚未创建组件实例,因此在setup
选项中没有this
。这意味着,除了props
之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。
使用 setup
函数时,它将接受两个参数:
props
context
让我们更深入地研究如何使用每个参数
1. Props
setup
函数中的第一个参数是props
。正如在一个标准组件中所期望的那样,setup
函数中的props
是响应式的,当传入新的prop
时,它将被更新
1// MyBook.vue
2
3export default {
4 props: {
5 title: String,
6 },
7 setup(props) {
8 console.log(props.title);
9 },
10};
注意:
但是,因为
props
是响应式的,你不能使用ES6
解构,因为它会消除prop
的响应性。
如果需要解构 prop,可以通过使用 setup
函数中的 toRefs
来安全地完成此操作。
1// MyBook.vue
2
3import { toRefs } from 'vue'
4
5setup(props) {
6 const { title } = toRefs(props)
7
8 console.log(title.value)
9}
2. 上下文
传递给
setup
函数的第二个参数是context
。context
是一个普通的 JavaScript 对象,它暴露三个组件的 property
1// MyBook.vue
2
3export default {
4 setup(props, context) {
5 // Attribute (非响应式对象)
6 console.log(context.attrs);
7
8 // 插槽 (非响应式对象)
9 console.log(context.slots);
10
11 // 触发事件 (方法)
12 console.log(context.emit);
13 },
14};
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对context
使用 ES6 解构
1// MyBook.vue export default { setup(props, { attrs, slots, emit }) { ... } }
attrs
和slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以attrs.x
或slots.x
的方式引用 property。请注意,与props
不同,attrs
和slots
是非响应式的。如果你打算根据attrs
或slots
更改应用副作用,那么应该在onUpdated
生命周期钩子中执行此操作。
3. setup 组件的 property
执行
setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
4. ref reactive 以及 setup 结合模板使用
在看setup
结合模板使用之前,我们首先得知道ref
和 reactive
方法。
如果 setup
返回一个对象则可以在模板中绑定对象中的属性和方法,但是要定义响应式数据的时候可以使用ref
, reactive
方法定义响应式的数据
错误写法:
1<template>
2{{msg}}
3<br>
4
5<button @click="updateMsg">改变etup中的msg</button>
6
7<br>
8</template>
9
10<script>
11export default {
12 data() {
13 return {
14
15 }
16 },
17 setup() {
18 let msg = "这是setup中的msg";
19 let updateMsg = () => {
20 alert("触发方法")
21 msg = "改变后的值"
22 }
23 return {
24 msg,
25 updateMsg
26 }
27 },
28
29}
30</script>
31
32<style lang="scss">
33.home {
34 position: relative;
35}
36</style>
正确写法一:
ref用来定义响应式的 字符串、 数值、 数组、
Bool
类型
1import { ref } from 'vue'
2
3<template>
4 {{ msg }}
5 <br />
6 <br />
7 <button @click="updateMsg">改变etup中的msg</button>
8 <br />
9 <br />
10 <ul>
11 <li v-for="(item, index) in list" :key="index">
12 {{ item }}
13 </li>
14 </ul>
15
16 <br />
17</template>
18
19<script>
20import { ref } from "vue";
21
22export default {
23 data() {
24 return {};
25 },
26 setup() {
27 let msg = ref("这是setup中的msg");
28
29 let list = ref(["马总", "李总", "刘总"]);
30
31 let updateMsg = () => {
32 alert("触发方法");
33 msg.value = "改变后的值";
34 };
35 return {
36 msg,
37 list,
38 updateMsg,
39 };
40 },
41};
42</script>
43
44<style lang="scss">
45.home {
46 position: relative;
47}
48</style>
正确写法二:
reactive 用来定义响应式的对象
1<template>
2<div>
3 <h1>解构响应式对象数据</h1>
4 <p>Username: {{username}}</p>
5 <p>Age: {{age}}</p>
6</div>
7</template>
8
9<script>
10import {
11 reactive,
12 toRefs
13} from "vue";
14
15export default {
16 name: "解构响应式对象数据",
17 setup() {
18 const user = reactive({
19 username: "张三",
20 age: 10000,
21 });
22
23 return {
24 ...toRefs(user)
25 };
26 },
27};
28</script>
**说明:**要改变 ref 定义的属性名称需要通过 属性名称.value
来修改,要改变reactive
中定义的对象名称可以直接
5. 使用 this
在
setup()
内部,this
不会是该活跃实例的引用,因为setup()
是在解析其它组件选项之前被调用的,所以setup()
内部的this
的行为与其它选项中的this
完全不同。这在和其它选项式 API 一起使用setup()
时可能会导致混淆
二、toRefs - 解构响应式对象数据
把一个响应式对象转换成普通对象,该普通对象的每个
property
都是一个ref
,和响应式对象property
一一对应
1<template>
2<div>
3 <h1>解构响应式对象数据</h1>
4 <p>Username: {{username}}</p>
5 <p>Age: {{age}}</p>
6</div>
7</template>
8
9<script>
10import {
11 reactive,
12 toRefs
13} from "vue";
14
15export default {
16 name: "解构响应式对象数据",
17 setup() {
18 const user = reactive({
19 username: "张三",
20 age: 10000,
21 });
22
23 return {
24 ...toRefs(user)
25 };
26 },
27};
28</script>
当想要从一个组合逻辑函数中返回响应式对象时,用 toRefs 是很有效的,该 API 让消费组件可以 解构 / 扩展(使用
…
操作符)返回的对象,并不会丢失响应性:
1function useFeatureX() {
2 const state = reactive({
3 foo: 1,
4 bar: 2,
5 });
6
7 // 对 state 的逻辑操作
8 // ....
9
10 // 返回时将属性都转为 ref
11 return toRefs(state);
12}
13
14export default {
15 setup() {
16 // 可以解构,不会丢失响应性
17 const { foo, bar } = useFeatureX();
18
19 return {
20 foo,
21 bar,
22 };
23 },
24};
三、computed - 计算属性
1
2<template>
3<div>
4 <h1>解构响应式对象数据+computed</h1>
5
6 <input type="text" v-model="firstName" placeholder="firstName" />
7 <br>
8 <br>
9 <input type="text" v-model="lastName" placeholder="lastName" />
10
11 <br>
12 {{fullName}}
13</div>
14</template>
15
16<script>
17import {
18 reactive,
19 toRefs,
20 computed
21} from "vue";
22
23export default {
24 name: "解构响应式对象数据",
25 setup() {
26 const user = reactive({
27 firstName: "",
28 lastName: "",
29 });
30
31 const fullName = computed(() => {
32 return user.firstName + " " + user.lastName
33 })
34
35 return {
36 ...toRefs(user),
37 fullName
38 };
39 },
40};
41</script>
42
四、readonly “深层”的只读代理
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的
1
2<template>
3 <div>
4 <h1>readonly - “深层”的只读代理</h1>
5 <p>original.count: {{original.count}}</p>
6 <p>copy.count: {{copy.count}}</p>
7 </div>
8</template>
9
10<script>
11import { reactive, readonly } from "vue";
12
13export default {
14 name: "Readonly",
15 setup() {
16 const original = reactive({ count: 0 });
17 const copy = readonly(original);
18
19 setInterval(() => {
20 original.count++;
21 copy.count++; // 报警告,Set operation on key "count" failed: target is readonly. Proxy {count: 1}
22 }, 1000);
23
24
25 return { original, copy };
26 },
27};
28</script>
29
五、watchEffect
在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。
1<template>
2 <div>
3 <h1>watchEffect - 侦听器</h1>
4 <p>{{ data.count }}</p>
5 <button @click="stop">手动关闭侦听器</button>
6 </div>
7</template>
8
9<script>
10import { reactive, watchEffect } from "vue";
11export default {
12 name: "WatchEffect",
13 setup() {
14 const data = reactive({
15 count: 1,
16 num: 1,
17 });
18 const stop = watchEffect(() => console.log(`侦听器:${data.count}`));
19 setInterval(() => {
20 data.count++;
21 }, 1000);
22 return {
23 data,
24 stop,
25 };
26 },
27};
28</script>
六、watch 、watch 与 watchEffect 区别
对比watchEffect
,watch
允许我们:
- 懒执行,也就是说仅在侦听的源变更时才执行回调;
- 更明确哪些状态的改变会触发侦听器重新运行;
- 访问侦听状态变化前后的值
更明确哪些状态的改变会触发侦听器重新运行
1<template>
2 <div>
3 <h1>watch - 侦听器</h1>
4 <p>count1: {{ data.count1 }}</p>
5 <p>count2: {{ data.count2 }}</p>
6 <button @click="stopAll">Stop All</button>
7 </div>
8</template>
9
10<script>
11import { reactive, watch } from "vue";
12export default {
13 name: "Watch",
14 setup() {
15 const data = reactive({
16 count1: 0,
17 count2: 0,
18 });
19 // 侦听单个数据源
20 const stop1 = watch(data, () =>
21 console.log("watch1", data.count1, data.count2)
22 );
23 // 侦听多个数据源
24 const stop2 = watch([data], () => {
25 console.log("watch2", data.count1, data.count2);
26 });
27 setInterval(() => {
28 data.count1++;
29 }, 1000);
30 return {
31 data,
32 stopAll: () => {
33 stop1();
34 stop2();
35 },
36 };
37 },
38};
39</script>
访问侦听状态变化前后的值
1
2<template>
3<div>
4 <h1>watch - 侦听器</h1>
5 <input type="text" v-model="keywords" />
6</div>
7</template>
8
9<script>
10import {
11 ref,
12 watch
13} from "vue";
14export default {
15 name: "Watch",
16 setup() {
17 let keywords = ref("111");
18 // 侦听单个数据源
19 watch(keywords, (newValue, oldValue) => {
20 console.log(newValue, oldValue)
21 });
22
23 return {
24 keywords
25 };
26 },
27};
28</script>
29
懒执行,也就是说仅在侦听的源变更时才执行回调
1
2<template>
3<div>
4 <h1>watch - 侦听器</h1>
5 <p>num1={{num1}}</p>
6 <p>num2={{num2}}</p>
7</div>
8</template>
9
10<script>
11import {
12 ref,
13 watch,
14 watchEffect
15} from "vue";
16export default {
17 name: "Watch",
18 setup() {
19 let num1 = ref(10);
20 let num2 = ref(10);
21 // 侦听单个数据源
22 watch(num1, (newValue, oldValue) => {
23 console.log(newValue, oldValue)
24 });
25
26 watchEffect(() => console.log(`watchEffect侦听器:${num2.value}`));
27
28 return {
29 num1,
30 num2
31 };
32 },
33};
34</script>
35
七、组合式 api 生命周期钩子
你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | 不需要* |
created | 不需要* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写
1// MyBook.vue
2
3export default {
4 props: {
5 title: String,
6 },
7 setup(props) {
8 console.log(props.title);
9 },
10};
八、Provider Inject
通常,当我们需要将数据从父组件传递到子组件时,我们使用 props。想象一下这样的结构:你有一些深嵌套的组件,而你只需要来自深嵌套子组件中父组件的某些内容。在这种情况下,你仍然需要将 prop 传递到整个组件链中,这可能会很烦人
对于这种情况,我们可以使用
provide
和inject
对父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个provide
选项来提供数据,子组件有一个inject
选项来开始使用这个数据
1. 非组合式 api 中的写法
1<!-- src/components/MyMap.vue -->
2<template>
3 <MyMarker />
4</template>
5
6<script>
7import MyMarker from "./MyMarker.vue";
8
9export default {
10 components: {
11 MyMarker,
12 },
13 provide: {
14 location: "North Pole",
15 geolocation: {
16 longitude: 90,
17 latitude: 135,
18 },
19 },
20};
21</script>
22<!-- src/components/MyMarker.vue -->
23<script>
24export default {
25 inject: ["location", "geolocation"],
26};
27</script>
2. 组合式 api 中的写法
Provider:
在
setup()
中使用provide
时,我们首先从vue
显式导入provide
方法。这使我们能够调用provide
时来定义每个property
provide
函数允许你通过两个参数定义 property
:
property
的name
(<String>
类型)property
的value
使用 MyMap
组件,我们提供的值可以按如下方式重构:
1
2<!-- src/components/MyMap.vue -->
3<template>
4 <MyMarker />
5</template>
6
7<script>
8import { provide } from 'vue'
9import MyMarker from './MyMarker.vue
10
11export default {
12 components: {
13 MyMarker
14 },
15 setup() {
16 provide('location', 'North Pole')
17 provide('geolocation', {
18 longitude: 90,
19 latitude: 135
20 })
21 }
22}
23</script>
Inject:
在
setup()
中使用inject
时,还需要从vue
显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。
inject
函数有两个参数:
- 要注入的
property
的名称 - 一个默认的值 (可选)
使用 MyMarker
组件,可以使用以下代码对其进行重构:
1
2<!-- src/components/MyMarker.vue -->
3<script>
4import { inject } from 'vue'
5
6export default {
7 setup() {
8 const userLocation = inject('location', 'The Universe')
9 const userGeolocation = inject('geolocation')
10
11 return {
12 userLocation,
13 userGeolocation
14 }
15 }
16}
17</script>
Provider Inject 响应性
父组件:
1
2import {
3 provide,
4 ref,
5 reactive
6} from 'vue'
7
8setup() {
9 const location = ref('北京')
10 const geolocation = reactive({
11 longitude: 90,
12 latitude: 135
13 })
14 const updateLocation = () => {
15 location.value = '上海'
16 }
17 provide('location', location);
18 provide('geolocation', geolocation);
19 return {
20 updateLocation
21 }
22 }
23
24
子组件:
1import { inject } from 'vue'
2
3export default {
4 setup() {
5 const userLocation = inject('location', 'The Universe')
6 const userGeolocation = inject('geolocation')
7
8 return {
9 userLocation,
10 userGeolocation
11 }
12 }
13}
14</script>