前言:前端渲染海量数据列表的情况可能不多,因为很多列表都采用了分页,但是在读写Excel表数据这种情况下,处理海量数据还是有可能会用到。渲染十万条数据,可以体现前端开发同学处理高性能渲染的能力,也是面试常常问到的问题。
对于一次性插入大量数据的情况,一般有两种做法:
- 分批渲染
- 虚拟列表
本次对使用 分批渲染 的方式进行实现
虚拟列表参考链接: 渲染十万条数据的方法之虚拟列表
分批渲染
React代码实现
- requestAnimationFrame 是一个浏览器提供的 API,用于在下一次重绘之前执行回调函数。它通常用于优化动画和其他需要高频率更新的操作(参考: requestAnimationFrame原理和使用)
- generateUniqueKey 可以生成唯一key,使 diff 算法高效,原理参考: js生成唯一标识符(例如key或者id)
- 由于结合动画帧进行递归分批渲染,数据量大,会处理比较长的时间,所以不要忘记处理内存泄漏,在页面卸载的时候用一个开关去跳出可能尚存的递归
import { useCallback, useEffect, useRef, useState } from 'react';
interface DataItem {
key?: string;
slogan: string;
bgColor: string;
}
/**
* 生成唯一 key,这里使用时间戳 + 随机数
* 你也可以引入第三方库,如 uuid 或 nanoid,但这里为了减少依赖,直接使用 JS 生成
* @returns
*/
const generateUniqueKey = () => {
return `${new Date().getTime()}-${Math.random().toString(36).substr(2, 9)}`;
};
const DATA_LIST = [
{ slogan: '我爱学友', bgColor: 'green' },
{ slogan: '我爱德华', bgColor: 'blue' },
{ slogan: '我爱黎明', bgColor: 'red' }
];
const BatchPage = () => {
const [list, setList] = useState<DataItem[]>([]);
// 用于组件卸载后,清除异步操作,防止内存泄漏
const isUnmountedRef = useRef<boolean>(false);
// 默认 batchSize = 100,即时间分片的每片为 100,每个动画帧渲染 100 条数据,可以根据实际情况调整
const renderBatch = useCallback(
(restList: DataItem[], existingList: DataItem[], batchSize = 100) => {
if (!Array.isArray(restList) || !restList?.length || isUnmountedRef.current) return;
// splice 除了改变原始数组,还会返回删掉的数组
const addList = restList.splice(0, batchSize);
// requestAnimationFrame 是一个浏览器提供的 API,用于在下一次重绘之前执行回调函数。它通常用于优化动画和其他需要高频率更新的操作。
requestAnimationFrame(() => {
const newList = [...existingList, ...addList];
setList(newList);
renderBatch(restList, newList, batchSize);
});
},
[]
);
const handleClick = useCallback(() => {
const jsMakeDataStartTime = new Date().getTime();
console.log('点击按钮时间戳-------->', jsMakeDataStartTime);
const data: DataItem[] = [];
// 模拟生成 10 万条数据
for (let index = 0; index < 100000; index++) {
const item = DATA_LIST[Math.floor(Math.random() * DATA_LIST.length)];
data.push({
key: generateUniqueKey(),
...item
});
}
const jsMakeDataEndTime = new Date().getTime();
console.log('JS生成数据时间戳------>', jsMakeDataEndTime);
console.log('JS生成数据时间间隔---->', jsMakeDataEndTime - jsMakeDataStartTime);
renderBatch(data, []);
}, [renderBatch]);
useEffect(() => {
if (!!list?.length) console.log('数据变化时间戳和长度-->', new Date().getTime(), list?.length);
}, [list]);
useEffect(() => {
isUnmountedRef.current = false;
return () => {
isUnmountedRef.current = true;
};
}, []);
// 本应使用【className + 引入样式文件】的方式,但为了直观,这里直接使用style演示
return (
<div
style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap'
}}
>
<button
style={{ padding: '12px', color: 'white', backgroundColor: 'black' }}
onClick={handleClick}
>
生成海量数据&渲染
</button>
<div style={{ flex: '1', overflowY: 'auto' }}>
{Array.isArray(list) &&
list.map(item => {
return (
<div
key={item?.key}
style={{
backgroundColor: item?.bgColor,
marginTop: '6px',
color: 'white'
}}
>
{item?.slogan}
</div>
);
})}
</div>
</div>
);
};
export default BatchPage;