下载
关键代码
- 拼接表头.
- requestAnimationFrame(processChunk),调用递归切片方法.
- 切片程序内部,拼接表格内容,更新进度条信息,定义递归结束条件.
- 进入递归结束后,调用下载方法,重置进度条信息.
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>
上传
- 将每一个文本切割
- 解析表头
- requestAnimationFrame(processChunk),调用递归切片方法.
- 切片程序内部,解析每一行数据,更新进度条信息,定义递归结束条件.
- 进入递归结束后,调用回调方法,重置进度条信息.
关键代码
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 | 利用浏览器空闲时间 | 不可预测的执行时间 | 低优先级后台任务 |
最佳实践建议
- 合理设置块大小 :通过
performance.now()
测量,确保每块执行时间<16ms - 优先处理可见数据:对于滚动列表,先处理视口内数据
- 结合虚拟滚动:对于超大列表,只渲染可见部分
- 错误恢复机制:记录已处理位置,意外中断后可恢复
- 内存管理:及时释放已处理数据引用
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
分块是在主线程上处理大量数据时的最佳平衡方案,能在保持页面响应性的同时完成数据处理任务。