场景
长列表的渲染,是前端很容易遇到的问题。常规的解决方案有滚动加载,分页等等。但是常规的滚动加载(没有做列表虚拟化),随着往下滚动加载的越多,div节点的数目也会变得更多,从而导致界面的性能问题,而分页加载虽然避开了性能问题,但体验没有滚动加载那么顺滑。所以是虚拟化的列表是较好的选择,它只会渲染视图范围内的div节点个数。
解决方案
virtualized List,以及antd 的组件有实现虚拟化列表的功能。
自己的虚拟化列表
虽然已有的库可以解决日常开发的问题,但如果因为项目原因不能引用第三方库呢,所以还需要化为自己的东西,所以这里就参考virtualized List的实现,手写一个简单的虚拟化列表。
原理
原理就是内外两个列表,外列表(视图列表)定高,内列表(总高列表)根据数据总数和行高算出总高度,然后滚动的时候,算出当前在内列表(总高列表)到顶部的距离,根据距离算出当前应该显示的列表的首行以及尾行,然后截取数组渲染。每行的定位为绝对定位,根据行数,算到顶部的距离。
代码
import React from 'react';
import ReactDOM from 'react-dom';
const LIST_HEIGHT = 640; // 列表的高度
const SINGLE_ROW_HEIGHT = 128; // 每行的高度
const ALL_DATA_LENGTH = 10000; // 后端返回的列表长度
const LOAD_MORE_LINE = 2; // 为了防止滚动出现空白,多渲染前后的行数。
const VIEW_LINE = 5; // 列表显示的行数
// 视图列表的样式 需要定高
const outerListStyle = {
height: LIST_HEIGHT,
overflow: 'auto',
background: '#ECECEC',
};
// 列表每一行div的样式 需要定高
const rowStyle = {
height: SINGLE_ROW_HEIGHT,
background: '#fff',
border: '1px solid',
position: 'absolute',
width: '300px',
};
// 获取所有列表的工具函数
const getInitData = (number) => {
const list = [];
for (let i = 0; i < number; i += 1) {
list.push({
index: i,
key: `list${i}`,
value: `row${i}`,
});
}
return list;
};
const allData = getInitData(ALL_DATA_LENGTH);
// 防抖
const debounce = (fn, delay = 200) => {
if (typeof fn !== 'function') {
// 参数类型为函数
throw new TypeError('fn is not a function');
}
let lastFn = null;
return function (...args) {
if (lastFn) {
clearTimeout(lastFn);
}
let lastFn = setTimeout(() => {
lastFn = null;
fn.call(this, ...args);
}, delay);
};
};
// 组件
class List extends React.Component {
componentDidMount() {
this.listDom = document.getElementById('outer-list');
this.listDom.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
this.listDom.removeEventListener('scroll', this.handleScroll);
}
state = {
viewData: allData.slice(0, VIEW_LINE),
};
// 滚动回调函数
handleScroll = debounce(() => {
const scrollTop = this.listDom.scrollTop;
const computeStarIndex = parseInt(scrollTop / SINGLE_ROW_HEIGHT);
const startIndex = computeStarIndex - LOAD_MORE_LINE > 0 // 根据滚动条滚动的距离 算当前应该显示的首行行数
? computeStarIndex - LOAD_MORE_LINE
: 0;
const compotedEndINdex = (computeStarIndex > startIndex ? computeStarIndex : startIndex)
+ LOAD_MORE_LINE
+ VIEW_LINE;
const endIndex = compotedEndINdex > ALL_DATA_LENGTH ? ALL_DATA_LENGTH : compotedEndINdex; //算加上前后多渲染行数的 末行行数
const viewData = allData.slice(startIndex, endIndex); // 截取对应区间 生成显示数组
this.setState({
viewData,
});
console.log('startIndex', startIndex);
console.log('endIndex', endIndex);
}, 500);
renderRow = (item) => {
const computerTop = item.index * SINGLE_ROW_HEIGHT;
return (
<div
key={item.key}
style={{
...rowStyle,
top: computerTop, //划重点 利用当前的行数和行高算到顶部的距离
}}
>
<div>{item.key}</div>
<div>{item.value}</div>
</div>
);
};
render() {
const { viewData } = this.state;
return (
//外层视图显示的列表
<div id="outer-list" style={outerListStyle} >
<div //内层列表,需要算所有数据的高度
id="inner-list"
style={{
height: SINGLE_ROW_HEIGHT * allData.length,
position: 'relative',
}}
>
{viewData.map(item => this.renderRow(item))}
</div>
</div>
);
}
}
ReactDOM.render(<List />, document.getElementById('container'));
总结
以上就是一个简单的虚拟化列表了。