将数组数据下载为 csv 文件,上传 csv 文件解析为数组

下载

关键代码

  1. 拼接表头.
  2. requestAnimationFrame(processChunk),调用递归切片方法.
  3. 切片程序内部,拼接表格内容,更新进度条信息,定义递归结束条件.
  4. 进入递归结束后,调用下载方法,重置进度条信息.
js 复制代码
function downloadLargeCSVInChunks(data, _headers, filename = 'data.csv', chunkSize = 1000, uploadProcess) {
  if (data.length === 0) return;

  const headers = _headers || Object.keys(data[0]);
  let csvContent = headers.join(',') + '\n';
  let currentIndex = 0;

  function processChunk() {
    const startTime = performance.now();
    let chunkData = '';
    let isFirstChunk = true;

    while (currentIndex < data.length &&
      (currentIndex % chunkSize !== 0 || isFirstChunk)
      && performance.now() - startTime < 16) {
      const row = data[currentIndex];
      const values = headers.map(header => {
        const escaped = ('' + row[header]).replace(/"/g, '""');
        return `"${escaped}"`;
      });
      chunkData += values.join(',') + '\n';
      currentIndex++;
      isFirstChunk = false;
    }

    csvContent += chunkData;

    const progress = Math.min(100, (currentIndex / data.length * 100));
    uploadProcess(progress);

    if (currentIndex < data.length) {
      requestAnimationFrame(processChunk);
    } else {
      finalizeDownload();
      uploadProcess(0);
    }
  }

  function finalizeDownload() {
    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
    const link = document.createElement('a');
    const url = URL.createObjectURL(blob);

    link.href = url;
    link.setAttribute('download', filename);
    document.body.appendChild(link);
    link.click();

    setTimeout(() => {
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
    }, 100);
  }

  requestAnimationFrame(processChunk);
}

测试用例

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="file" accept=".csv" id="csvFile" />
  <script>
    function generateLargeData(size) {
      const data = [];
      for (let i = 0; i < size; i++) {
        data.push({
          id: i,
          name: 'name' + i,
          age: 18
        })
      }
      return data;
    }
    
    const veryLargeData = generateLargeData(500000); // 假设有50万条数据
    downloadLargeCSVInChunks(
      veryLargeData,
      ['id', 'name', 'age'],
      'very_large_data.csv',
      10000,
      (progress) => {
        console.log('下载进度:', progress.toFixed(2), '%');
      }
    );
  </script>
</body>

</html>

上传

  1. 将每一个文本切割
  2. 解析表头
  3. requestAnimationFrame(processChunk),调用递归切片方法.
  4. 切片程序内部,解析每一行数据,更新进度条信息,定义递归结束条件.
  5. 进入递归结束后,调用回调方法,重置进度条信息.

关键代码

js 复制代码
function parseLargeCSVInChunks(csvText, chunkSize = 1000, uploadProcess, callback) {
  const lines = csvText.split(/\r?\n/).filter(line => line.trim() !== '');
  if (lines.length === 0) return callback([]);

  const headers = lines[0].split(',');
  const result = [];
  let currentIndex = 1;

  function processChunk() {
    const startTime = performance.now();
    let isFirstChunk = true;
    while (currentIndex < lines.length &&
      (currentIndex % chunkSize !== 0 || isFirstChunk)
      && performance.now() - startTime < 16) {
      const line = lines[currentIndex];
      const values = line.split(',');
      const obj = {};

      headers.forEach((header, i) => {
        obj[header.trim()] = values[i] ? values[i].trim() : '';
      });

      result.push(obj);
      currentIndex++;
    }

    const progress = Math.min(100, (currentIndex / lines.length * 100));
    uploadProcess(progress);

    if (currentIndex < lines.length) {
      requestAnimationFrame(processChunk);
    } else {
      callback(result);
      uploadProcess(0);
    }
  }

  requestAnimationFrame(processChunk);
}

测试用例

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="file" accept=".csv" id="csvFile" />
  <script>
    // ... 【解析代码】
    const csvFile = document.getElementById("csvFile");
    csvFile.addEventListener("change", CsvUploader);
    function CsvUploader(event) {
      const file = event.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (e) => {
        // type: string
        const text = e.target.result;
        parseLargeCSVInChunks(text, 1000, (progress) => {
          console.log('解析进度:', progress.toFixed(2), '%');
        }, (result) => {
          console.log('解析完成:', result);
        });
      };
      reader.readAsText(file);
    };
  </script>
</body>

</html>

requestAnimationFrame 分块处理的优势

requestAnimationFrame 分块处理大数据量操作(如 CSV 解析/渲染)相比传统同步处理方式具有以下显著优势:

1. 保持页面流畅性(避免卡顿)

核心优势:将长时间运行的任务分割成多个小任务,在浏览器每一帧渲染间隙执行,确保主线程不被阻塞。

  • 传统方式:一次性处理10万行数据 → 主线程被阻塞3-5秒 → 页面冻结
  • rAF分块:每帧处理1000行 → 主线程每帧只阻塞10-15ms → 保持60fps流畅度

2. 智能调度与浏览器协同工作

与浏览器渲染周期同步

  • 自动适应浏览器的重绘周期(通常60fps,即每16.7ms一帧)
  • 当浏览器忙于渲染时,会自动延迟下一批处理
  • 当页面处于非激活状态(标签页切换)时,会自动暂停处理
javascript 复制代码
function processChunk() {
  const start = performance.now();
  
  // 处理数据块(控制在16ms以内)
  do {
    // 处理单条数据...
  } while (performance.now() - start < 16);
  
  if (hasMoreData) {
    requestAnimationFrame(processChunk); // 下一帧继续
  }
}

3. 避免脚本执行时间过长

防止浏览器警告/终止

  • 现代浏览器会终止执行时间过长的脚本(如Chrome的"长任务"机制,>50ms的任务会被标记)
  • 分块处理确保每个任务片段都是短任务

4. 精确的进度控制

可实现细粒度进度反馈

javascript 复制代码
let processed = 0;
const total = 100000;

function processChunk() {
  const start = performance.now();
  
  while (/* 条件 */) {
    // 处理数据...
    processed++;
    
    // 每处理100条更新一次进度
    if (processed % 100 === 0) {
      updateProgress(processed / total * 100);
    }
  }
  
  if (processed < total) {
    requestAnimationFrame(processChunk);
  }
}

5. 内存效率更高

相比Web Worker的替代方案

  • 不需要复制大量数据到Worker线程
  • 适合中等规模数据(10万-100万条)
  • 避免了Worker通信开销

6. 用户体验优化

可实现以下效果

  • 实时进度条更新
  • 可中途取消操作
  • 保持页面其他交互响应
javascript 复制代码
let cancelFlag = false;

document.getElementById('cancel-btn').addEventListener('click', () => {
  cancelFlag = true;
});

function processChunk() {
  if (cancelFlag) {
    console.log('用户取消了操作');
    return;
  }
  
  // ...处理逻辑
  
  if (!cancelFlag && hasMoreData) {
    requestAnimationFrame(processChunk);
  }
}

对比其他分块技术

技术 优点 缺点 适用场景
requestAnimationFrame 与渲染周期同步,避免卡顿 仍占用主线程 中等数据量(10万条内)的UI相关操作
setTimeout 简单实现分块 调度不精确,可能仍阻塞 简单后台任务
Web Worker 完全不阻塞主线程 通信开销大,数据需要序列化 超大数据量(100万条+)
Idle Callback 利用浏览器空闲时间 不可预测的执行时间 低优先级后台任务

最佳实践建议

  1. 合理设置块大小 :通过performance.now()测量,确保每块执行时间<16ms
  2. 优先处理可见数据:对于滚动列表,先处理视口内数据
  3. 结合虚拟滚动:对于超大列表,只渲染可见部分
  4. 错误恢复机制:记录已处理位置,意外中断后可恢复
  5. 内存管理:及时释放已处理数据引用
javascript 复制代码
// 优化后的分块处理框架
function chunkProcessor(data, processItem, chunkSize, onComplete) {
  let index = 0;
  const total = data.length;
  
  function processNextChunk() {
    const startTime = performance.now();
    let processed = 0;
    
    while (index < total && processed < chunkSize && performance.now() - startTime < 16) {
      processItem(data[index], index);
      index++;
      processed++;
    }
    
    if (index < total) {
      requestAnimationFrame(processNextChunk);
    } else {
      onComplete?.();
    }
  }
  
  processNextChunk();
}

requestAnimationFrame分块是在主线程上处理大量数据时的最佳平衡方案,能在保持页面响应性的同时完成数据处理任务。

相关推荐
好运的阿财39 分钟前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
XiYang-DING1 小时前
JavaScript
开发语言·javascript·ecmascript
空中海2 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海2 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
空中海3 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
杨超凡3 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript
threelab4 小时前
Three.js 咖啡杯烟雾效果 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
Heo5 小时前
14_React 中的更新队列 updateQueue
前端·javascript·面试
前端 贾公子5 小时前
解决浏览器端 globalThis is not defined 报错
前端·javascript·vue.js