通用 Loading 状态管理器

一、设计背景与核心目标

1.1 场景痛点

  • 并发冲突:多个业务请求(如表格刷新 + 表单提交)同时触发 Loading,样式配置不一致导致视觉混乱。
  • 配置维护难:如果每个调用处都传一堆参数,后期修改默认行为(如全局最小显示时间)将是一场灾难。
  • 闪烁问题:请求过快完成时,Loading 瞬间消失会产生视觉闪烁,影响体验。

1.2 核心设计决策

设计点 解决方案 意图
预设模板化 导出 LOADING_PRESETS 实现"零参数"调用,统一常用场景配置。
首次注册锁定 相同 type 仅首次生效配置 避免同一业务场景下配置打架,简化逻辑。
优先级队列 processQueue 动态决策 确保高优先级(如弹窗)能覆盖低优先级(如表格)。
全局计时起点 globalStartTime 保证 minTime 从 Loading 首次出现开始计算,而非最后一次。

二、完整实现代码(带 JSDoc)

typescript 复制代码
import { ref, shallowRef } from "vue";
​
// 优先级映射表:将语义化字符串转换为数字,方便比较
const LOADING_PRIORITY = { low: 1, medium: 2, high: 3 };
​
/**
 * @typedef {Object} LoadingConfig
 * @property {string} type - 唯一标识符,用于区分不同的 Loading 实例
 * @property {'low'|'medium'|'high'} priority - 优先级,决定多 Loading 并存时的展示顺序
 * @property {number} minTime - 最小显示时长(毫秒),避免闪烁
 * @property {string} loadingType - 视觉样式标识(如 'transparent', 'fullscreen')
 */
​
/**
 * 通用预定义配置模板
 * 仅根据"优先级"和"视觉样式"组合
 * @type {Record<string, LoadingConfig>}
 */
export const LOADING_PRESETS = {
  // 基础透明类
  transparent_low:    { type: 'trans_low',  priority: 'low',    minTime: 0,   loadingType: 'transparent' },
  transparent_medium: { type: 'trans_med',  priority: 'medium', minTime: 300, loadingType: 'transparent' },
  transparent_high:   { type: 'trans_high', priority: 'high',   minTime: 500, loadingType: 'transparent' },
​
  // 基础全屏类
  fullscreen_low:    { type: 'full_low',  priority: 'low',    minTime: 0,   loadingType: 'fullscreen' },
  fullscreen_medium: { type: 'full_med',  priority: 'medium', minTime: 300, loadingType: 'fullscreen' },
  fullscreen_high:   { type: 'full_high', priority: 'high',   minTime: 500, loadingType: 'fullscreen' },
};
​
/**
 * 通用 Loading 状态管理器
 * 支持并发计数、优先级决策及最短显示时间控制
 */
export function useLoading() {
  // 存储各 type 的实时状态:Map<type, LoadingEntry>
  // LoadingEntry 结构: { priority: number, minTime: number, loadingType: string, count: number }
  const typeMap = shallowRef(new Map()); 
  
  const loading = ref(false);                 // 全局 Loading 开关
  const loadingType = ref('transparent');     // 当前应展示的视觉样式类型
  let globalStartTime = null;                 // Loading 首次开启的时间戳(闭包变量)
​
  /**
   * 配置解析器:支持"预设 Key"或"动态对象"
   * @param {string|LoadingConfig} configOrKey 
   * @returns {LoadingConfig}
   */
  const resolveConfig = (configOrKey) => {
    if (typeof configOrKey === 'string') {
      // 优先匹配通用预设,若无则退化为默认的低优先级透明 Loading
      return LOADING_PRESETS[configOrKey] || { type: configOrKey, priority: 'low', minTime: 0, loadingType: 'transparent' };
    }
    // 动态传入对象时,以传入值为准,未传字段给默认值
    return { type: 'dynamic', priority: 'low', minTime: 0, loadingType: 'transparent', ...configOrKey };
  };
​
  /**
   * 开启 Loading
   * @param {string|LoadingConfig} [configOrKey='transparent_medium'] - 预设 Key 或配置对象
   */
  const showLoading = (configOrKey = 'transparent_medium') => {
    const config = resolveConfig(configOrKey);
    const { type } = config;
​
    if (!type) return console.warn('[useLoading] type is required');
​
    const entry = typeMap.value.get(type);
    if (entry) {
      // 【首次注册锁定】已存在该 type,忽略新配置,仅增加并发计数
      entry.count += 1; 
    } else {
      // 首次出现,以当前配置进行"注册"
      typeMap.value.set(type, { 
        priority: LOADING_PRIORITY[config.priority], 
        minTime: config.minTime, 
        loadingType: config.loadingType,
        count: 1 
      });
      // 记录全局起点(仅在没有任何 Loading 时记录)
      if (globalStartTime === null) globalStartTime = Date.now();
    }
    processQueue();
  };
​
  /**
   * 关闭 Loading
   * @param {string} type - 业务标识,需与 showLoading 时使用的 type 一致
   */
  const hideLoading = (type) => {
    if (!type) return console.warn('[useLoading] type is required');
    const entry = typeMap.value.get(type);
    if (!entry) return;
​
    if (entry.count <= 1) {
      // 该 type 最后一个请求完成,计算剩余显示时间
      const timeLeft = Math.max(0, entry.minTime - (Date.now() - globalStartTime));
      setTimeout(() => {
        typeMap.value.delete(type);
        // 若 Map 清空,重置全局起点
        if (typeMap.value.size === 0) globalStartTime = null;
        processQueue();
      }, timeLeft);
    } else {
      // 仍有并发请求,仅减少计数
      entry.count -= 1;
    }
  };
​
  /**
   * UI 决策逻辑:根据优先级和插入顺序决定展示哪个 Loading
   * 规则:高优先级 > 低优先级;同优先级 > 先来后到
   */
  const processQueue = () => {
    if (typeMap.value.size === 0) { 
      loading.value = false; 
      return; 
    }
    
    // 1. 寻找最高优先级
    let maxPriority = 0;
    for (const e of typeMap.value.values()) {
      if (e.priority > maxPriority) maxPriority = e.priority;
    }
    
    // 2. 在同优先级中,选择最先插入的那个 type(Map 迭代顺序保证)
    for (const [type, e] of typeMap.value.entries()) {
      if (e.priority === maxPriority) {
        loading.value = true;
        loadingType.value = e.loadingType; // 更新为获胜者的视觉类型
        return;
      }
    }
  };
​
  return { showLoading, hideLoading, loadingType, loading };
}

三、数据结构与关键变量

3.1 核心状态

  • typeMap : Map<type, Entry>。管理所有活跃 Loading 的生命周期。
  • loadingType: 响应式变量,UI 组件通过它判断是渲染"透明遮罩"还是"全屏旋转"。
  • globalStartTime : 闭包变量。解决连续请求时 minTime 累加的问题,确保最短时长从第一次亮起开始算。

3.2 Map 条目结构 (Entry)

typescript 复制代码
{
  priority: number,      // 数字化后的优先级 (1-3)
  minTime: number,       // 最小显示毫秒数
  loadingType: string,   // 视觉样式标识 (如 'transparent')
  count: number          // 该 type 下的并发请求计数
}

四、核心流程详解

4.1 开启逻辑 (showLoading)

  1. 解析配置:将传入的字符串或对象转换为标准配置。

  2. 查重与注册

    • type 已存在:静默忽略 新配置,count++
    • type 不存在:写入 Map,锁定配置,并初始化 globalStartTime
  3. 触发决策 :调用 processQueue 重新评估当前应该展示谁。

4.2 关闭逻辑 (hideLoading)

  1. 计数递减 :若 count > 1,说明还有兄弟请求在跑,直接返回。
  2. 延迟清理 :若 count === 1,计算 timeLeft。即使接口 10ms 就完了,也要等够 minTime 才从 Map 删除。
  3. 状态重置 :删除条目后,若 Map 为空,关闭全局 loading 并清除时间戳。

4.3 决策逻辑 (processQueue)

  • 原则:高优先级 > 低优先级;同优先级 > 先来后到。
  • 执行 :遍历 Map 找到优先级最高的条目,将其 loadingType 赋值给全局状态。

五、典型使用场景

场景 1:极简调用(推荐)

scss 复制代码
// 自动套用预设:medium 优先级,300ms 最短时间,透明样式
showLoading('transparent_medium'); 

场景 2:特殊定制

php 复制代码
// 临时需要一个全屏且至少显示 1s 的 Loading
showLoading({ type: 'special-report', priority: 'high', loadingType: 'fullscreen', minTime: 1000 });

场景 3:并发覆盖

php 复制代码
showLoading({ type: 't1', priority: 'low', loadingType: 'transparent' }); // 此时展示 transparent
setTimeout(() => {
  showLoading({ type: 't2', priority: 'high', loadingType: 'fullscreen' }); // 优先级更高,UI 自动切换为 fullscreen
}, 100);

六、总结与建议

  1. 通用性优先:预设只包含视觉和行为维度(透明度、优先级)。
  2. 约定优于配置 :尽量使用导出的 LOADING_PRESETS,减少手动传参带来的配置不一致风险。
  3. 静默处理冲突 :对于相同 type 的不同配置,采用"首次锁定"策略,保持逻辑轻量化。
相关推荐
RONIN2 小时前
vue插件--路由vue-router
vue.js
Ruihong2 小时前
Vue Suspense 组件在 React 中,VuReact 会如何实现?
vue.js·react.js·面试
胡志辉2 小时前
网络七层到底怎么落到一次前端请求上:从浏览器到网卡,再到远端服务器
前端·网络协议
怪兽同学2 小时前
统一管理Agent Skills
前端·agent
雪芽蓝域zzs2 小时前
uni-app x 使用 UTS 语言使用 mixins
开发语言·javascript·uni-app
陆枫Larry2 小时前
微信小程序订阅消息完全指南:从原理到落地的全流程梳理
前端
DaqunChen2 小时前
全栈开发的演变:从LAMP到MEAN再到现代JavaScript
开发语言·javascript·ecmascript
Camellia-lon2 小时前
jQuery购物车实现:从入门到精通
前端·javascript·jquery
Mintopia2 小时前
一套能落地的"模块拆分"方法:不靠经验也能做对
前端