Vue 全局 Loading 解决方案:优雅处理并发请求
一、 问题背景:常规方案的局限性
在许多项目中,全局 Loading 是一个非常普遍的需求。一个基础的实现方案通常如下:
定义一个全局状态:使用
ref创建一个布尔类型的响应式变量来控制 Loading 的显示和隐藏。javascript// store/loading.js import { ref } from 'vue'; export const isLoading = ref(false);在请求拦截器中控制状态:在发送请求前,将
isLoading设置为true;请求结束后(无论成功或失败),将其设置为false。javascript// api/axios.js import { isLoading } from '@/store/loading'; // 请求拦截器 axios.interceptors.request.use(config => { isLoading.value = true; return config; }); // 响应拦截器 axios.interceptors.response.use(response => { isLoading.value = false; return response; }, error => { isLoading.value = false; return Promise.reject(error); });在根组件中使用:将 Loading 组件的显示与
isLoading状态绑定。vue<!-- App.vue --> <template> <router-view /> <GlobalLoading v-if="isLoading" /> </template>
这个方案在处理单个请求时表现良好。但是,当应用中存在 并发请求 时,问题就暴露出来了。
并发场景下的问题:
假设页面同时触发了两个 API 请求(请求 A 和 请求 B)。
- 请求 A 开始 ->
isLoading变为true,Loading 动画显示。 - 请求 B 开始 ->
isLoading仍为true。 - 请求 A (耗时3秒) 先返回 ->
isLoading被置为false,Loading 动画 立即消失。 - 请求 B (耗时5秒) 仍在处理中。
结果:用户看到 Loading 动画在第二个请求还未完成时就提前消失了,这不符合预期。Loading 应该在 所有 并发请求都完成后才消失。
二、 核心思路:引用计数法
为了解决上述问题,我们不能简单地使用一个布尔值来跟踪状态。正确的思路是 引用计数(Reference Counting)。
- 维护一个计数器
loadingCount,初始值为0。 - 每当一个请求 开始 时,将
loadingCount加 1。 - 每当一个请求 结束 时,将
loadingCount减 1。 - Loading 动画的显示与否,取决于
loadingCount是否大于0。
这样,只有当最后一个请求返回,loadingCount 变为 0 时,Loading 动画才会消失。
三、 技术实现:巧用 customRef 封装逻辑
Vue 3 提供的 customRef API 非常适合用来封装这种带有计数逻辑的自定义响应式状态。customRef 允许我们显式地控制依赖跟踪 (track) 和更新触发 (trigger)。
下面是使用 customRef 实现带引用计数的全局 Loading 状态的代码:
// store/loading.js
import { customRef } from 'vue';
const useLoading = () => {
let loadingCount = 0; // 计数器变量,不直接暴露
// customRef 接收一个工厂函数
return customRef((track, trigger) => {
return {
// get 访问器:当读取 .value 时触发
get() {
track(); // 1. 收集依赖:告诉 Vue 这个值被读取了
return loadingCount > 0; // 2. 返回计算后的布尔值
},
// set 设置器:当写入 .value 时触发
set(value) {
// 3. 根据传入的布尔值更新计数器
if (value) {
loadingCount++;
} else {
loadingCount--;
}
// 4. 边界处理:防止计数器变为负数
if (loadingCount < 0) {
loadingCount = 0;
}
// 更优雅的写法:
// loadingCount = Math.max(0, loadingCount);
trigger(); // 5. 触发更新:通知 Vue 状态已改变,需要更新视图
}
};
});
};
export const isLoading = useLoading();代码解析:
track(): 在get方法中调用。当isLoading.value被读取时,Vue 会记录下是哪个组件或副作用函数依赖了这个状态。get返回值: 返回的不再是原始值,而是loadingCount > 0这个动态计算出来的布尔值。这是整个方案的核心。set(value): 当执行isLoading.value = true或isLoading.value = false时,set方法被调用。我们在这里不直接存储true或false,而是用它来控制loadingCount的增减。- 边界处理: 如果因为某些原因(例如,响应拦截器被意外执行多次),
loadingCount减成了负数,我们需要将其重置为0,以避免逻辑错误。 trigger(): 在set方法中调用。当loadingCount发生变化后,通知所有依赖isLoading的地方进行更新。
现在,请求拦截器的逻辑保持不变,但其行为已经因为我们自定义的 ref 而变得正确了。
四、 代码优化
set 方法中的 if/else 逻辑可以被简化为一行:
// ...
set(value) {
// value 为 true 时加 1,为 false 时加 -1
loadingCount += value ? 1 : -1;
loadingCount = Math.max(0, loadingCount); // 边界处理
trigger();
}
// ...这种写法更简洁,意图也同样清晰。
五、 总结
通过使用 Vue 3 的 customRef,我们成功地将复杂的 引用计数 逻辑封装在一个行为与普通 ref 一致的响应式变量中。这不仅解决了并发请求下的全局 Loading 问题,也提供了一个高度可复用且优雅的解决方案,充分体现了组合式 API 的灵活性和强大能力。