场景描述
当在一些展示性页面的时候,会经常性使用一些列表进行渲染,但是当数据太多的时候,dom 节点不断累加,会造成滚动时页面的卡顿,影响用户体验
解决方案:虚拟列表
核心思想
虚拟列表只对可视区域中的列表进行渲染, 滚动时改变渲染的数组
子列表元素高度固定时
- 整个列表高度固定
- 可视区域渲染条数固定
- 可根据滚动距离,得到渲染的数组
- 移动可视区域到滚动的距离位置
当有10000条数据,屏幕高度为500时, 子元素高度为50,那么可渲染区域应该渲染为10条数据, 整个列表高度为50 * 10000
当我们发生滚动的时候,比如滚动了150px,那么我们可见区域的渲染列表就变成了下图 第4项到第13项了
结构
<div className={styles.page} style={{height: pageHeight + "px"}} ref="container"> {/* 屏幕高度 */}
<div className={styles.infiniteListGhost} style={{height: infiniteListGhostHeight + "px"}} ></div> {/* list 高度 */}
<div className={styles.renderList} style={{ transform: `translate3d(0, ${translate}px, 0)`}} > {/* 可见list 高度 */}
{
renderList.map((item, index) => {
return (
<div className={styles.item} key={index} style={{height: itemHeight + "px"}}> {/* 子列表元素高度 */}
{ item }
</div>
)
})
}
</div>
</div>
.page {
overflow-y: auto;
width: 100%;
position: relative;
.infiniteListGhost {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: -1;
}
.renderList {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 1;
.item {
color: #000;
border: 1px solid #ccc;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
}
子列表项的高度为 itemHeight = 50, 滚动高度 为 scrollTop
- 屏幕高度:
pageHeight = document.body.clientHeight
; - 列表高度:
infiniteListGhostHeight = list.length * itemHeight
- 渲染条数:
const itemCount = Math.ceil(clientHeight / itemHeight );
- startIndex:
startIndex = Math.floor(scrollTop / itemHeight)
- endIndex:
endIndex = startIndex + itemCount
- 列表渲染数组:
list.slice(startIndex, endIndex)
; - startOffset:
startOffset = scrollTop - (scrollTop % itemHeight)
; 滚动倍数
state = {
pageHeight: 0, //屏幕高度
infiniteListGhostHeight: 0, // 列表总高度
renderList: [], // 渲染列表
itemHeight: 80,
translate: 0, // 可视区域偏移
}
componentDidMount() {
const { itemHeight } = this.state
this.refs.container.addEventListener('scroll', this.handleScroll);
const clientHeight = document.body.clientHeight;
const itemCount = Math.ceil(clientHeight / itemHeight );
this.setState({
pageHeight: clientHeight,
infiniteListGhostHeight: result.length * itemHeight,
renderList: result.slice(0, itemCount)
})
}
componentWillUnmount() {
this.refs.container.removeEventListener('scroll', this.handleScroll);
}
handleScroll = (e) => {
const { itemHeight, pageHeight, infiniteListGhostHeight } = this.state
const scrollTop = e.srcElement.scrollTop || e.srcElement.scrollTop;
// 从scrollTop 计算出偏移startIndex
const itemCount = Math.ceil(pageHeight / itemHeight ); //可视区域高度 / 子项高度 = 子项个数
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + itemCount;
const list = result.slice(startIndex, endIndex );
const startOffset = (scrollTop - (scrollTop % itemHeight));
this.setState({
translate: startOffset,
renderList: list
})
}
效果
可以看出,只渲染可视区域内的数据
设置上下缓存区
当滚动太块的时候,往下会有一段空白, 往上也有一段空白,那么这时候设置上下缓冲区可以解决此问题
handleScroll = (e) => {
// ....
const above = Math.min(startIndex, itemCount);
const below = Math.min(result.length - endIndex, itemCount);;
const start = startIndex - above;
const end = endIndex + below;
const list = result.slice(start, end); // 注意此时list的渲染会加上缓存区,所以导致了偏移向下了,但实际上应该减掉 上方缓冲区才能渲染中间的
const startOffset = (scrollTop - (scrollTop % itemHeight) - above * itemHeight);
}
此时效果: