# 组合式函数
组合式函数,本质上也就是代码复用的一种方式。
- 组件:对结构、样式、逻辑进行复用
- 组合式函数:侧重于对 有状态 的逻辑进行复用
# 快速上手
实现一个鼠标坐标值的追踪器。
<template>
<div>当前鼠标位置: {{ x }}, {{ y }}</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
</script>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
多个组件中复用这个相同的逻辑,该怎么办?
答:使用组合式函数。将包含了状态的相关逻辑,一起提取到一个单独的函数中,该函数就是组合式函数。
# 相关细节
1. 组合式函数本身还可以相互嵌套
2. 和 Vue2 时期 mixin 区别
解决了 Vue2 时期 mixin 的一些问题。
不清晰的数据来源:当使用多个 minxin 的时候,实例上的数据属性来自于哪一个 mixin 不太好分辨。
命名空间冲突:如果多个 mixin 来自于不同的作者,可能会注册相同的属性名,造成命名冲突
mixin
const mixinA = { methods: { fetchData() { // fetch data logic for mixin A console.log("Fetching data from mixin A"); }, }, }; const mixinB = { methods: { fetchData() { // fetch data logic for mixin B console.log("Fetching data from mixin B"); }, }, }; new Vue({ mixins: [mixinA, mixinB], template: ` <div> <button @click="fetchData">Fetch Data</button> </div> `, });
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组合式函数:
// useMixinA.js import { ref } from "vue"; export function useMixinA() { function fetchData() { // fetch data logic for mixin A console.log("Fetching data from mixin A"); } return { fetchData }; } // useMixinB.js import { ref } from "vue"; export function useMixinB() { function fetchData() { // fetch data logic for mixin B console.log("Fetching data from mixin B"); } return { fetchData }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23组件使用上面的组合式函数:
import { defineComponent } from "vue"; import { useMixinA } from "./useMixinA"; import { useMixinB } from "./useMixinB"; export default defineComponent({ setup() { // 这里必须要给别名 const { fetchData: fetchDataA } = useMixinA(); const { fetchData: fetchDataB } = useMixinB(); fetchDataA(); fetchDataB(); return { fetchDataA, fetchDataB }; }, template: ` <div> <button @click="fetchDataA">Fetch Data A</button> <button @click="fetchDataB">Fetch Data B</button> </div> `, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22隐式的跨 mixin 交流
mixin
export const mixinA = { data() { return { sharedValue: "some value", }; }, };
1
2
3
4
5
6
7export const minxinB = { computed: { dValue() { // 和 mixinA 具有隐式的交流 // 因为最终 mixin 的内容会被合并到组件实例上面,因此在 mixinB 里面可以直接访问 mixinA 的数据 return this.sharedValue + "xxxx"; }, }, };
1
2
3
4
5
6
7
8
9组合式函数:交流就是显式的
import { ref } from "vue"; export function useMixinA() { const sharedValue = ref("some value"); return { sharedValue }; }
1
2
3
4
5
6import { computed } from "vue"; export function useMixinB(sharedValue) { const derivedValue = computed(() => sharedValue.value + " extended"); return { derivedValue }; }
1
2
3
4
5
6<template> <div> {{ derivedValue }} </div> </template> <script> import { defineComponent } from "vue"; import { useMixinA } from "./useMixinA"; import { useMixinB } from "./useMixinB"; export default defineComponent({ setup() { const { sharedValue } = useMixinA(); // 两个组合式函数的交流是显式的 const { derivedValue } = useMixinB(sharedValue); return { derivedValue }; }, }); </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 异步状态
根据异步请求的情况显示不同的信息:
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
<script setup>
import { ref } from "vue";
// 发送请求获取数据
const data = ref(null);
// 错误
const error = ref(null);
fetch("...")
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err));
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如何复用这段逻辑?仍然是提取成一个组合式函数。
如下:
import { ref } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err));
return { data, error };
}
2
3
4
5
6
7
8
9
10
11
12
现在重构上面的组件:
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
<script setup>
import { useFetch } from "./hooks/useFetch";
const { data, error } = useFetch("xxxx");
</script>
2
3
4
5
6
7
8
9
10
11
12
13
这里为了更加灵活,我们想要传递一个响应式数据:
const url = ref("first-url");
// 请求数据
const { data, error } = useFetch(url);
// 修改 url 的值后重新请求数据
url.value = "new-url";
2
3
4
5
此时我们就需要重构上面的组合式函数:
import { ref, watchEffect, toValue } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const fetchData = () => {
// 每次执行 fetchData 的时候,重制 data 和 error 的值
data.value = null;
error.value = null;
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err));
};
watchEffect(() => {
fetchData();
});
return { data, error };
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 约定和最佳实践
1. 命名:组合式函数约定用驼峰命名法命名,并以“use”作为开头。例如前面的 useMouse、useEvent.
2. 输入参数:注意参数是响应式数据的情况。如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它能够被正确追踪,请确保要么使用 watch( ) 显式地监视 ref 或 getter,要么在 watchEffect( ) 中调用 toValue( )。
3. 返回值
组合式函数中推荐返回一个普通对象,该对象的每一项是 ref 数据,这样可以保证在解构的时候仍然能够保持其响应式的特性:
// 组合式函数
export function useMouse() {
const x = ref(0);
const y = ref(0);
// ...
return { x, y };
}
2
3
4
5
6
7
8
9
import { useMouse } from "./hooks/useMouse";
// 可以解构
const { x, y } = useMouse();
2
3
如果希望以对象属性的形式来使用组合式函数中返回的状态,可以将返回的对象用 reactive 再包装一次即可:
import { useMouse } from "./hooks/useMouse";
const mouse = reactive(useMouse());
2
4. 副作用
在组合式函数中可以执行副作用,例如添加 DOM 事件监听器或者请求数据。但是请确保在 onUnmounted 里面清理副作用。
例如在一个组合式函数设置了一个事件监听器,那么就需要在 onUnmounted 的时候移除这个事件监听器。
export function useMouse() {
// ...
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
// ...
}
2
3
4
5
6
7
8
也可以像前面 useEvent 一样,专门定义一个组合式函数来处理副作用:
import { onMounted, onUnmounted } from "vue";
export function useEventListener(target, event, callback) {
// 专门处理副作用的组合式函数
onMounted(() => target.addEventListener(event, callback));
onUnmounted(() => target.removeEventListener(event, callback));
}
2
3
4
5
6
7
5. 使用限制
只能在 <script setup>或 setup( ) 钩子中调用:确保在组件实例被创建时,所有的组合式函数都被正确初始化。特别如果你使用的是选项式 API,那么需要在 setup 方法中调用组合式函数,并且返回,这样才能暴露给 this 及其模板使用
import { useMouse } from "./mouse.js"; import { useFetch } from "./fetch.js"; export default { setup() { // 因为组合式函数会返回一些状态 // 为了后面通过 this 能够正确访问到这些数据状态 // 必须在 setup 的时候调用组合式函数 const { x, y } = useMouse(); const { data, error } = useFetch("..."); return { x, y, data, error }; }, mounted() { // setup() 暴露的属性可以在通过 `this` 访问到 console.log(this.x); }, // ...其他选项 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18只能被同步调用:组合式函数需要同步调用,以确保在组件实例的初始化过程中,所有相关的状态和副作用都能被正确地设置和处理。如果组合式函数被异步调用,可能会导致在组件实例还未完全初始化时,尝试访问未定义的实例数据,从而引发错误。
可以在像 onMounted 生命周期钩子中调用:在某些情况下,可以在如 onMounted 生命周期钩子中调用组合式函数。这些生命周期钩子也是同步执行的,并且在组件实例已经被初始化后调用,因此可以安全地使用组合式函数。