将数组数据下载为 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分块是在主线程上处理大量数据时的最佳平衡方案,能在保持页面响应性的同时完成数据处理任务。

相关推荐
快起来别睡了32 分钟前
深入浅出 Event Loop:前端工程师必须掌握的运行机制
前端·javascript
user2975258761233 分钟前
别再用关键字搜了!手搓一个Vite插件,为页面上的标签打上标记
前端·javascript·vite
典学长编程35 分钟前
前端开发(HTML,CSS,VUE,JS)从入门到精通!第五天(jQuery函数库)
javascript·css·ajax·html·jquery
尝尝你的优乐美1 小时前
原来前端二进制数组有这么多门道
前端·javascript·面试
前端_yu小白1 小时前
Vue2实现docx,xlsx,pptx预览
开发语言·javascript·ecmascript
金金金__1 小时前
事件循环-原理篇
javascript·浏览器
CF14年老兵2 小时前
2025 年 React 在 Web 开发中的核心地位:优势、应用场景与顶级案例
javascript·react.js·redux
还是大剑师兰特2 小时前
Javascript面试题及详细答案150道之(046-060)
javascript·大剑师·js面试题
橙某人2 小时前
📆基于Grid布局完成最精简的日期组件
前端·javascript
Likeyou72 小时前
HTML无尽射击小游戏包含源码,纯HTML+CSS+JS
javascript·css·html