大数据表单元格批量请求优化方案

背景

在表格组件中,每个单元格是一个vue实例,数据请求、数据处理、数据展示都由组件内部管理,虽然原则上实现了数据的解耦,但是也引入了一个问题,即数据的请求次数与单元格的数量呈正相关,导致在大量单元格渲染的场景下,请求发送特别频繁

优化思路

仔细观察表格内的场景以及单元格的请求格式,会发现两个特点:

  1. 同一列的单元格请求的数据接口是一样的,参数类似
  2. 数据接口往往支持批量处理

既然如此,是否可以将一定时间段内的请求参数合并去重后再提交,以达到减少请求的目的

方案

请求队列

此方案适合参数根据单元格的具体内容填入请求参数的场景

方案流程:

  1. 各单元格将要请求的数据推送到批量请求模块中,通过回调函数等待回调数据
  2. 请求队列通过一个worker中的定时触发器来控制消费速度
  3. 调度信号来了以后,将当前请求数据合并去重,然后发起请求
  4. 请求成功后将数据抛给回调函数,触发组件更新

为什么要把队列触发器放在worker?

大数据场景下,单元格渲染很慢。如使用setTimeout防抖会导致发送请求的时间推迟到所有单元格渲染完成,出现长时间白屏;如使用setInterval节流,主线程被渲染卡住导致setInterval一直被延后到所有单元格渲染完成,也会出现长时间白屏。这里采用把请求队列的调度放到worker里的方案

代码示例

javascript 复制代码
const MAX_REQUEST_COUNT = 100;
const queue = {
	codes: [],
	callbacks: []
}

// 外部调用
const batchGetter = (
    codes: string[],
    callback: (data: any[]) => void,
) => {
  queue.codes = queue.codes.concat(codes);
  queue.callbacks.push(callback);
  worker.postMessage({
    method: 'start',
    id: 'batchGetter',
    interval: 100,
  });
};

// 发起请求
const startRequest = async () => {
  const len = queue.codes.length;

  if (!len) {
    worker.postMessage({
      method: 'stop',
      id: 'batchGetter',
    });
    return;
  }

  const valueSet = new Set(queue.codes);
  const fns = queue.callbacks.slice(0, len);
  queue.codes.splice(0, len);
  queue.callbacks.splice(0, len);

  if (valueSet.size) {
    let res: any = [];
    try {
      const response = await Promise.allSettled(
        chunk([...valueSet], MAX_REQUEST_COUNT).map((codes) => {
          return api({ codes });
        }),
      );

      const data: any = [];
      response.forEach((item) => {
        if (item.status === 'fulfilled') {
          data.push(...item.value);
        }
      });
      res = data || [];
    } catch (e) {
      res = [];
    }
    for (const fn of fns) {
      fn(res);
    }
  } else {
    for (const fn of fns) {
      fn([]);
    }
  }
};

// worker中的队列调度
const idMap: Record<string, any> = {};
addEventListener('message', (e: MessageEvent) => {
  const { id, method, interval = 100 } = e.data;
  if (method === 'start') {
    if (!idMap[id]) {
      idMap[id] = setInterval(() => {
        postMessage({
          id,
          method: 'run',
        });
      }, interval);
    }
  } else if (method === 'stop') {
    clearInterval(idMap[id]);
    delete idMap[id];
  }
});

// 主线程监听
worker.addEventListener('message', (e) => {
  const { id, method } = e.data;

  if (method === 'run') {
    switch (id) {
      case 'batchGetter':
        startRequest();
        break;
    }
  }
});

请求复用

该方案适合参数固定的请求,拉取内容保持一致,可缓存

方案流程

  1. 发起请求后,根据函数名 + 参数列表生成一个标记符
  2. 如果发现标记符能在数据缓存记录中找到,直接返回缓存的数据
  3. 如果发现标记符能在请求缓存记录中找到,返回缓存的请求
  4. 发起请求,将当前promise保存到请求缓存记录中,请求结束后将返回内容缓存到数据缓存记录中,并把promise从请求缓存记录中删掉

代码示例

javascript 复制代码
const apiCache = new Map<string, Promise<any>>();
const resultCache = new Map<string, any>();
const lruQueue: string[] = [];
const LRU_MAX = 100;

const batchGetFullData = (
  api: (params: any, silent: boolean) => Promise<any>,
  params: AnalyserNode,
) => {
  const key = `${api?.name || 'default'}_${JSON.stringify(params)}`;

  if (resultCache.has(key)) {
    lruQueue.splice(lruQueue.indexOf(key), 1);
    lruQueue.unshift(key);
    return Promise.resolve(resultCache.get(key));
  }

  if (apiCache.has(key)) {
    return apiCache.get(key);
  }

  const promise = new Promise((resolve) => {
    api(params, true)
      .then((res) => {
        lruQueue.splice(lruQueue.indexOf(key), 1);
        lruQueue.unshift(key);
        while (lruQueue.length > LRU_MAX) {
          const removeKey = lruQueue.pop();
          if (removeKey) {
            resultCache.delete(removeKey);
          }
        }
        resultCache.set(key, res);
        apiCache.delete(key);
        resolve(res);
      })
      .catch(() => {
        lruQueue.splice(lruQueue.indexOf(key), 1);
        apiCache.delete(key);
        resultCache.delete(key);
        resolve([]);
      });
  });
  apiCache.set(key, promise);
  return promise;
};
相关推荐
Ausra无忧几秒前
记录在公司把单服务器升级成多服务器架构流程
前端·后端·架构
极客密码12 分钟前
来看看我用Codex两周时间vibe coding的这款轻量级的剪贴板管理应用,win/mac系统均可用
前端·ai编程·vibecoding
前端双越老师14 分钟前
Agent 实战: 智语 + baoyu-skills 自动发布文章到公众号
前端·agent·全栈
hunterandroid24 分钟前
Jetpack Compose 入门:用声明式 UI 写 Android 页面
前端
以和为贵26 分钟前
前端手写 RAG 踩坑实录:四个让检索"翻车"的坑
前端·人工智能·面试
用户21366100357231 分钟前
Redux异步方案与React性能优化Hooks
前端
假如让我当三天老蒯33 分钟前
TypeScript 继续学习(学习用)
前端·面试·typescript
玄玄子33 分钟前
CSS 浮动引起父元素高度塌陷
前端·css
拾年27535 分钟前
我用 30 行代码,搞懂了大模型是怎么"读"中文的
javascript·人工智能·llm
竹林81836 分钟前
从 ethers.js 到 viem:我在一个 DeFi 看板项目中踩过的所有坑与最终方案
前端·javascript