如果接口返回的数据特别慢?要怎么办?难道就要在当前页面一直等吗

最近做AI应用相关的新功能有这样一个业务场景:

  1. 用户输入特定信息生成指标
  2. 根据生成的指标结合用户再次输入的特定信息生成报告
  3. 生成报告的过程很久,快的时候大概要7~8分钟,慢的时候要10多分钟
    (注:指标和报告的生成都是通过AI算法)

当时做的第一个版本如图,在页面右侧有一个历史栏,可以看到已经生成的指标和报告数据。但是正在生成的数据是不会显示在右侧历史栏里的(这也是后续优化的一个点)。

这就会导致如果一个报告正在生成,用户想实时追踪这个报告的状态的话就只能留在当前页面,和冰冷的提示语大眼对小眼。

如果这时候你切换到其他页面或者其他已经生成的报告,在页面上就点不回来了,因为还在生成中的报告是不会显示在历史数据中的,这样给用户体验就非常不好。

而且,由于算法的问题这个接口很耗时会超时报错,实际上报告可能在超时报错之后才真正生成出来,但这时候停留在当前页面的用户就永远拿不到这条数据了。这里就比较严重了,也很影响测试!

于是产品拉上我和后端大哥开了个小会,确定要在报告生成时就显示在历史数据里以及一些其他需求。加了需求之后我们开发讨论修改逻辑为:生成报告时先调用一个接口返回一个 id,但没有内容,为了实现产品加的需求。之后新写一个接口 getReportDetail 采用轮询调用来查报告是否生成,停留在当前页面就进行轮询,切换到其他页面就停止轮询。

这样子就解决了痛点,因为报告会在后台自己生成,等到生成后他会更新在历史记录中,哪怕历史记录中的数据是旧的,在切换到当前页面时,前端判断如果这个报告正在生成,那就会立即调用一次 getReportDetail,来进行双重保险。

部分代码如下:

js 复制代码
    // 轮询方法封装
    const startPolling = async (currentReportId: string, originalUpdateTime: string) => {
      let intervalId: NodeJS.Timeout | null = null;
      const waitTime = 3 * 60 * 1000;

      const pollImmediately = async () => {
        try {
          await poll();
        } catch (error) {
          handlePollingError(intervalId);
        }
      };

      const poll = async () => {
        try {
          const currentNewRiskId = sessionStorage.getItem('currentNewRiskId') || riskId;
          const isSameReport = currentNewRiskId === currentReportId;

          // 如果不是同一个报告,终止轮询
          if (!isSameReport) {
            if (intervalId) {
              clearInterval(intervalId);
              intervalId = null;
            }
            return;
          }

          const detail = await getReportDetail(currentReportId);
          const isReady =
            detail.report &&
            new Date(detail.updateTime).getTime() >
              new Date(originalUpdateTime).getTime() + waitTime;
          if (isReady) {
            clearInterval(intervalId!);

            // 重新生成完成后从数组中移除ID
            removeRefreshingId(currentReportId);

            batchUpdate(() => {
              updateRiskState(detail);
              setEditTime(detail.updateTime);
              setUpdateReportName(!updateReportName);
            });

            setGeneratingState(false);
            setReportError(false);
          }
        } catch (error) {
          handlePollingError(intervalId);
        }
      };

      pollImmediately();

      intervalId = setInterval(poll, 60000);

      // 存储轮询ID到ref
      pollingIntervalRef.current = intervalId;

      return intervalId;
    };

调用轮询方法的部分:

由于报告还有重新生成的逻辑,所以分为了已有报告和没有报告两个分支

需要注意的地方还有:在切换另一个还在生成的报告的时候要清除上一个报告的轮询,以及组件卸载的时候停止轮询。

在代码中封装了一些公共方法,比如像上述的两个报告分支都会更新的数据就可以放到里面,然后更新数据的代码可以放到 batchUpdate 里。

js 复制代码
    
    // 状态更新封装
    const updateRiskState = (detail: IReportDetail) => {
      setSelectedIndicatorId(detail.templateId ?? '');
      setRiskRecordContent(detail.report ?? '');
      setReportId(detail.id ?? '');
      setRiskLevel(detail.riskLevel ?? '');
      setCreateName(detail.name ?? '');
    };
    
    batchUpdate(() => {
      updateRiskState(riskRecord);
      setIndexName(riskRecord.templateName);
      setMiningName(riskRecord.areaName);
      setText(riskRecord.descriptionInput);
      setEditTime(riskRecord.updateTime ?? riskRecord.createTime);
      setShared(riskRecord.shared === 1);
      setRecordUserId(riskRecord.userId);
   });

这里可以顺便展开介绍下 batchUpadate,

batchUpadate 是 React 的​​关键性能优化策略​ ​,它将多个状态更新操作合并为单个更新操作,从而​​显著减少不必要的渲染次数​ ​。 如果上面代码不使用 batchUpdate ,这将导致​​多次独立的渲染过程​​,造成界面闪烁和性能损耗。

实现原理

js 复制代码
    const batchUpdate = (updater: () => void) => {
      ReactDOM.unstable_batchedUpdates(updater);
    };

这里的核心是调用了React内部的unstable_batchedUpdates方法,它会:

  1. 将包裹的回调函数放入更新队列
  2. 合并所有状态变更
  3. 最终执行一次渲染过程

潜在陷阱与解决方案

​问题1:回调中访问过期的状态​

javascript 复制代码
batchUpdate(() => {
  setCount(count + 1); // 使用闭包值
  setTotal(total + count); // 可能访问过期值
});

// 解决方案:使用函数式更新
batchUpdate(() => {
  setCount(prev => prev + 1);
  setTotal(prev => prev + count); 
});

​问题2:滥用批处理导致UI响应延迟​

javascript 复制代码
// 错误做法:将耗时操作放入批处理
batchUpdate(() => {
  processHugeData(); // 阻塞UI
  setResults(data);
});

// 正确:仅状态更新用批处理
processHugeDataAsync().then(result => {
  batchUpdate(() => setResults(result));
});
相关推荐
@大迁世界39 分钟前
43.HTML 事件处理和 React 事件处理有什么区别?
前端·javascript·react.js·html·ecmascript
CloneCello39 分钟前
AI时代程序员认知调整指南
前端
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_38:(DocumentFragment 文档片段接口详解)
前端·javascript·ui·html·音视频
@大迁世界2 小时前
41.ShadCN 是什么?它如何和 Tailwind CSS 集成,从而更容易构建可访问且可自定义的 React 组件?
前端·javascript·css·react.js·前端框架
千叶风行3 小时前
Text-to-SQL 技术设计与注意事项
前端·人工智能·后端
软件开发技术深度爱好者3 小时前
HTML5+JavaScript读取DOCX 文档完整内容
前端·html5
幽络源小助理3 小时前
苹果CMS V10 MXPro V4.5模版下载, 自适应视频主题源码, 幽络源源码
前端·开源·源码·php源码
kyriewen4 小时前
坏了,黑客学会用AI写外挂了
前端·程序员·ai编程
xiangxiongfly9154 小时前
Vue3 根据角色权限动态加载路由
前端·javascript·vue.js·动态加载路由
爱吃的小肥羊4 小时前
Claude Code 推出Agent View,一个人同时指挥十个 AI 写代码!
aigc·ai编程