Vue 3组合式函数(Composable)最佳实践
组合式函数(Composable)是一个封装、复用有状态的代码逻辑的函数。类似Vue 2中的混入(Mixin)。组合式函数只能在 <script setup>
中被调用。
既然组合式函数也是一个函数,那么它的签名——函数名、参数列表、返回值和普通函数有什么区别呢?
export function useA(p, options) {
const {
immediate = true,
} = options || {};
const a = ref(null);
async function getA() {
a.value = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(
Array.from({length: 10}, (v, i) => i).map((i) => {
return `${toValue(p)}-a-${i + 1}`;
})
);
}, 2000);
});
}
if (immediate) {
getA();
}
return {
a,
getA
};
}
函数名
函数名约定以“use”作为开头。
参数列表
参数列表分成必选参数(例如:p
)和可选参数(例如:options
)2个部分,其中:
- 必选参数:最好是ref引用类型,当参数值变化时,可以捕获到新旧值;可搭配
toValue()
使用。 - 可选参数:推荐用Object类型,和长的参数列表相比,能忽略参数顺序,方便添加新参数;
返回值
上述定义提到组合式函数封装了状态。状态就是一个ref引用类型的变量(例如:a
),之所以把状态定义成ref,是因为可以用于template中。
通常,约定返回值是一个包含多个状态的(ref)普通对象。也可以通过可选参数(例如:options
)动态决定是返回一个值,还是返回一个普通对象。
异步编程
组合式函数是处理、复用API调用的最优解。借助组合式函数,可忽略async、await等API调用过程,只关心API返回结果。常见的API调用场景如下:
- 单个API调用,例如:API
useA()
返回a
,侦听器监控a
,若a
的值非空,则格式化a
,最终,页面显示a
; - 多个API组合,例如:API
useA()
返回a
,侦听器监控a
,若a
的值非空,则把a
作为输入参数调用APIuseB()
,APIuseB()
返回b
,侦听器监控b
,若b
的值非空,则格式化b
,最终,页面显示b
;
第1个场景非常简单,不用多说。
第2个场景相对复杂。充斥着大量的侦听器,一方面用来处理API返回结果,另一方面起到串联后继API的作用。侦听器不但嵌套,而且可能会冲突、失效,尤其是把API `useA()`和API `useB()`作为某个按钮单击事件的回调函数时。侦听器嵌套尚且可以通过由后继API支持接收异步参数来解决。侦听器冲突、失效就棘手了!!!
当然,也不是完全无解,一种解决方案是:让侦听器只负责处理API返回结果,由触发器来负责显式调用后继API。具体的做法就是,组合式函数返回值除了包含状态,还要包含触发器——该状态对应的更新函数(例如:getA()
),在监控到API useA()
返回值a
变化时,显式调用API useB()
的触发器getB()
来获取b
!
完整的示例如下:
export function useA(p, options) {
const {
immediate = true,
} = options || {};
const a = ref(null);
async function getA() {
a.value = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(
Array.from({length: 10}, (v, i) => i).map((i) => {
return `${toValue(p)}-a-${i + 1}`;
})
);
}, 2000);
});
}
if (immediate) {
getA();
}
return {
a,
getA
};
}
export function useB(a, options) {
const {
immediate = true,
} = options || {};
const b = ref(null);
async function getB() {
b.value = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(
toValue(a).map(((a2) => {
return Array.from({length: 10}, (v, i) => i).map((i) => {
return `${a2}-b-${i + 1}`;
})
}))
)
}, 2000);
});
}
if (immediate) {
getB();
}
return {
b,
getB
};
}
<template>
<input type="text" v-model="p"/>
<input type="button" value="Test" @click="onBtnClick"/>
<div>a: </div>
<div>b: </div>
</template>
<script setup>
import {ref, watch, toValue} from 'vue'
import {useA} from '@/apis/a';
import {useB} from '@/apis/b';
const p = ref(null);
const {a, getA} = useA(p, {immediate: false});
const {b, getB} = useB(a, {immediate: false});
watch(
a,
(value) => {
if (value) {
getB();
}
},
{
immediate: true,
deep: true
}
);
function onBtnClick() {
getA();
}
</script>