数据量较大且无法使用分页方式来加载的列表。比如淘宝的商品列表页,一次请求10个商品,一次请求10个商品和50个商品数据返回所需要的时间相差不大。但是却会多出4次的接口请求,造成资源浪费。
进程是系统进行资源分配和调度的一个独立单位,一个进程内包含多个线程。常说的 JS 是单线程的,是指 JS 的主进程是单线程的。
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)
- 【分片渲染】 启用使用API setTimeout 分片渲染 每次渲染50条,进入宏任务列队进行页面渲染以提高效率。
- 开发一个【虚拟列表】组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X86iqJj4-1656642284323)(./img/img1.jpg)]
长列表的渲染有以下几种情况:
1、列表项高度固定,且每个列表项高度相等
2、列表项高度固定不相等,但组件调用方可以明确的传入形如(index: number)=>number的getter方法去指定每个索引处列表项的高度
3、列表项高度不固定,随内容适应,且调用方无法确定具体高度
每种情况大致思路相似,计算出totalHeight撑起容器,并在滚动事件触发时根据scrollTop值不断更新startIndex以及endIndex,以此从列表数据listData中截取元素。
核心代码
private onScroll() {
this.setState({scrollTop: this.container.current?.scrollTop || 0});
}
private calcListToDisplay(params: {
scrollTop: number,
listData: any[],
itemHeight: number,
bufferNumber: number,
containerHeight: number,
}) {
const {scrollTop, listData, itemHeight, bufferNumber, containerHeight} = params;
// 考虑到buffer
let startIndex = Math.floor(scrollTop / itemHeight);
startIndex = Math.max(0, startIndex - bufferNumber); //计算出 带buffer 的数据起点 取最大值防止起点为负数
const displayCount = Math.ceil(containerHeight / itemHeight);
let lastIndex = startIndex + displayCount;
lastIndex = Math.min(listData.length, lastIndex + bufferNumber); //计算出 带buffer 的数据终点,取最小值防止数据溢出
return {
data: listData.slice(startIndex, lastIndex + 1), //截取的数据
offset: startIndex * itemHeight //顶部偏移量
}
}
render() {
const {itemHeight, listData, height: containerHeight, bufferNumber = 10} = this.props;
const {scrollTop} = this.state;
const totalHeight = itemHeight * listData.length;
const { data: listToDisplay, offset } =
this.calcListToDisplay({scrollTop, listData, itemHeight, bufferNumber, containerHeight});
return (
{
listToDisplay.map((item, index) => {
return (
{item.text}
)
})
}
)
}
由于传入了Getter方法,相当于已知每个列表项的高度。我们可以维护一个数组posInfo来存储每个节点到容器顶部的距离,posInfo[i]即为第i项距离顶部的偏移量。
那么不考虑bufferNumber,只需要找出满足posInfo[k] < scrollTop,且posInfo[k+1] > scrollTop的k即可,由于posInfo一定是递增序列,可以采用二分法查找提高效率。
private onScroll() {
this.setState({scrollTop: this.container.current?.scrollTop || 0});
}
componentWillMount() {
const { listData, heightGetter } = this.props;
if (heightGetter instanceof Function) {
this.initItemPosition(listData, heightGetter);
}
}
private initItemPosition(listData: any[], heightGetter: heightGetter) {
this.totalHeight = listData.reduce((total: number, item: any, index: number) => {
const height = heightGetter(index);
this.posInfo.push(total);
return total + height;
}, 0);
}
private getListToDisplay(params: {
scrollTop: number;
listData: any[];
posInfo: number[];
containerHeight: number;
bufferNumber: number;
}) {
const { scrollTop, listData, posInfo, containerHeight, bufferNumber } = params;
let startIndex = this.searchPos(posInfo, scrollTop);
let lastIndex = listData.length - 1;
const lastIndexDistance = containerHeight + scrollTop;
for (let index = startIndex; index < listData.length; index++) {
if (posInfo[index] >= lastIndexDistance) {
lastIndex = index;
break;
}
}
// 考虑buffer
startIndex = Math.max(0, startIndex - bufferNumber);
lastIndex = Math.min(listData.length - 1, lastIndex + bufferNumber);
return {
data: listData.slice(startIndex, lastIndex + 1),
offset: posInfo[startIndex]
}
}
private searchPos(posInfo: number[], scrollTop: number) {
const _binarySearchPos = (start: number, end: number): number => {
if (end - start <= 1) {
return start;
}
const mid = Math.ceil((start + end) / 2);
if (posInfo[mid] === scrollTop) {
return mid;
} else if (posInfo[mid] < scrollTop) {
if (posInfo[mid + 1] && posInfo[mid + 1] >= scrollTop) {
return mid;
} else {
return _binarySearchPos(mid + 1, end);
}
} else {
if (posInfo[mid - 1] && posInfo[mid - 1] <= scrollTop) {
return mid - 1;
} else {
return _binarySearchPos(start, mid - 1);
}
}
}
return _binarySearchPos(0, posInfo.length - 1);
}
render() {
const {itemHeight, listData, height: containerHeight, bufferNumber = 10} = this.props;
const {scrollTop} = this.state;
const totalHeight = itemHeight * listData.length;
const { data: listToDisplay, offset } =
this.calcListToDisplay({scrollTop, listData, itemHeight, bufferNumber, containerHeight});
return (
{
listToDisplay.map((item, index) => {
return (
{item.text}
)
})
}
)
}
{ return listData[index].height }} listData={listData} />
核心代码
componentWillMount() {
this.heightCache = new Array(this.props.listData.length).fill(this.props.fuzzyItemHeight || 30);
}
onMount(index: number, height: number) {
if (index > this.lastCalcIndex) {
this.heightCache[index] = height; // heightCache数组存储已挂载过的列表项的高度
this.lastCalcIndex = index; //lastCalcIndex记录最后一个已挂载节点的索引
this.lastCalcTotalHeight += height; //lastCalcTotalHeight记录已挂载节点的全部高度和
//趋于准确
this.totalHeight = this.lastCalcTotalHeight + (this.props.listData.length - 1 - this.lastCalcIndex) * (this.props.fuzzyItemHeight || 30);
}
}
private getListToDisplay(params: {
scrollTop: number;
containerHeight: number;
itemHeights: number[];
bufferNumber: number;
listData: any[];
}) {
const { scrollTop, containerHeight, itemHeights, bufferNumber, listData } = params;
let calcHeight = itemHeights[0]; //初始化(已遍历的节点总高度) 值为 第一个已遍历节点的高度
let startIndex = 0;
let lastIndex = 0;
const posInfo = []; // posInfo记录各节点到顶部的距离
posInfo.push(0);
for (let index = 1; index < itemHeights.length; index++) {
//已遍历节点的总高度 > scrollTop滚动距离
if (calcHeight > scrollTop) {
startIndex = index - 1;
break;
}
posInfo.push(calcHeight);
calcHeight += itemHeights[index];
}
for (let index = startIndex; index < itemHeights.length; index++) {
if (calcHeight > scrollTop + containerHeight) {
lastIndex = index;
break;
}
calcHeight += itemHeights[index];
}
startIndex = Math.max(0, startIndex - bufferNumber);
lastIndex = Math.min(itemHeights.length - 1, lastIndex + bufferNumber);
return {
data: listData.slice(startIndex, lastIndex + 1),
offset: posInfo[startIndex]
}
}
render() {
const { height: containerHeight, listData, bufferNumber = 10 } = this.props;
const { scrollTop } = this.state;
const { data: _listToDisplay, offset } = this.getListToDisplay({ scrollTop, listData, itemHeights: this.heightCache, containerHeight, bufferNumber });
return (
{
_listToDisplay.map((item, index) => {
return (
{/* */}
{item.text}
)
})
}
)
}
react-virtualized
如果使用react开发,可以使用antdesign官网推荐的组件,结合 react-virtualized 实现滚动加载无限长列表,带有虚拟化(virtualization)功能,能够提高数据量大时候长列表的性能。