场景描述
当在一些展示性页面的时候,会经常性使用一些列表进行渲染,但是当数据太多的时候,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); 
} 
此时效果:
