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

最近做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));
});
相关推荐
爱生活的苏苏16 分钟前
vue生成二维码图片+文字说明
前端·vue.js
拉不动的猪18 分钟前
安卓和ios小程序开发中的兼容性问题举例
前端·javascript·面试
炫彩@之星24 分钟前
Chrome书签的导出与导入:步骤图
前端·chrome
贩卖纯净水.35 分钟前
浏览器兼容-polyfill-本地服务-优化
开发语言·前端·javascript
前端百草阁41 分钟前
从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
前端·vue.js·npm
夏日米米茶41 分钟前
Windows系统下npm报错node-gyp configure got “gyp ERR“解决方法
前端·windows·npm
且白1 小时前
vsCode使用本地低版本node启动配置文件
前端·vue.js·vscode·编辑器
程序研1 小时前
一、ES6-let声明变量【解刨分析最详细】
前端·javascript·es6
siwangqishiq22 小时前
Vulkan Tutorial 教程翻译(四) 绘制三角形 2.2 呈现
前端
李三岁_foucsli2 小时前
js中消息队列和事件循环到底是怎么个事,宏任务和微任务还存在吗?
前端·chrome