在日常开发中全局 Loading 是一个看似简单、实则容易翻车的问题。本文分享一种基于 Pinia 的全局 Loading 管理方案彻底解决多请求并发时的闪烁问题并通过封装 Composable 让接入成本降到最低。问题从哪里来最常见的做法是在 axios 拦截器里控制 Loading// 请求拦截器instance.interceptors.request.use(config{showLoading()returnconfig})// 响应拦截器instance.interceptors.response.use(response{hideLoading()returnresponse})单个请求时没问题。但页面一旦同时发出多个请求问题就来了请求A 发出 → showLoading() 请求B 发出 → showLoading()重复调用 请求A 完成 → hideLoading() ← Loading 消失了但 B 还没完成 请求B 完成 → hideLoading()结果就是 Loading 一闪而过或者中途消失又出现用户体验很差。计数器方案可以缓解这个问题——请求开始时count完成时count--等count 0再关闭。但这种方案脆弱一旦某个请求抛出异常没有走到count--Loading 就永远不会关闭。换一个思路与其在 axios 层面计数不如让每个组件管理自己的 loading 状态由一个中心 store 统一聚合只要有任意一个组件在加载全屏 Loading 就显示所有组件都完成了才关闭。这个逻辑用 Vue 的响应式系统来表达非常自然。实现第一步loading-store.jsimport{ref,computed,watch,toValue}fromvueimport{defineStore}frompiniaimport{ElLoading}fromelement-plusexportconstuseLoadingStoredefineStore(loading,(){constloadingSourcesref([])letloadingInstancenullconstaddLoadingSource(source){loadingSources.value.push(source)}constremoveLoadingSource(source){constindexloadingSources.value.indexOf(source)if(index-1)loadingSources.value.splice(index,1)}// 任意一个 source 为 trueisLoading 就为 trueconstisLoadingcomputed(()loadingSources.value.some(sourcetoValue(source)))// 自动驱动全屏 Loading 的显示与关闭watch(isLoading,(loading){if(loading){loadingInstanceElLoading.service({fullscreen:true})}else{loadingInstance?.close()loadingInstancenull}})return{addLoadingSource,removeLoadingSource}})有几个细节值得注意toValue的作用它能统一处理普通值、ref、以及 getter 函数三种情况是unref的加强版。这样loadingSources数组里存放的类型就不受限制更灵活。第二步封装 Composable如果直接在每个组件里手动调用addLoadingSource和removeLoadingSource容易忘记清理造成内存泄漏。把这两步封装进一个 Composable让 Vue 的生命周期自动处理// composables/useLoading.jsimport{ref,onBeforeUnmount}fromvueimport{useLoadingStore}from/stores/loading-storeexportfunctionuseLoading(){constloadingStoreuseLoadingStore()constisLoadingref(false)// 自动注册loadingStore.addLoadingSource(isLoading)// 组件卸载时自动移除不会忘onBeforeUnmount((){loadingStore.removeLoadingSource(isLoading)})returnisLoading}第三步组件里使用接入只需一行script setup import { useLoading } from /composables/useLoading const myLoading useLoading() const fetchData async () { myLoading.value true try { await api.getData() } finally { myLoading.value false // finally 确保出错也会执行 } } /script template button :disabledmyLoading clickfetchData刷新/button /template组件只需关心自己的状态完全不需要知道全局 Loading 的存在。为什么这样更好彻底解决闪烁。多个组件同时发请求时只要有一个还在加载全屏 Loading 就不会关闭。全部完成后才统一关闭不会出现中途消失的情况。出错也安全。每个组件在finally块里重置自己的状态即使请求抛出异常Loading 也能正确关闭。不像计数器方案一个异常就可能导致计数永远不归零。接入成本低。封装成 Composable 之后新组件接入只需一行useLoading()注册和清理全部自动处理。组件职责清晰。每个组件只管自己的myLoading不直接操作全局 UI。全局 Loading 的显示逻辑集中在 store 里方便统一修改比如换一套 Loading 组件。数据流一览组件A: myLoading true 组件B: myLoading true ↓ loadingSources [true, true, false] ↓ isLoading computed true ↓ watch 触发 → ElLoading 显示 --- 组件A: myLoading false 组件B: myLoading false ↓ loadingSources [false, false, false] ↓ isLoading computed false ↓ watch 触发 → instance.close()局限性这套方案也有它不适用的场景使用前需要了解只支持全屏 Loading。如果需要局部 Loading比如某个按钮转圈、某个卡片区域的骨架屏这套方案不够用仍需在组件内单独处理。依赖组件生命周期。Loading 状态和组件绑定跨组件的请求比如路由跳转期间触发的请求需要额外处理。小结这套方案的核心思路是用响应式系统替代手动计数让 Vue 自己去追踪哪些组件还在加载而不是靠开发者维护一个容易出错的计数器。封装成 Composable 之后新增组件接入的成本几乎可以忽略不计同时也不用担心忘记清理导致内存泄漏。对于中大型项目来说这是一个值得推广的实践。