场景描述

当在一些展示性页面的时候,会经常性使用一些列表进行渲染,但是当数据太多的时候,dom 节点不断累加,会造成滚动时页面的卡顿,影响用户体验

解决方案:虚拟列表

核心思想

虚拟列表只对可视区域中的列表进行渲染, 滚动时改变渲染的数组

子列表元素高度固定时

  • 整个列表高度固定
  • 可视区域渲染条数固定
  • 可根据滚动距离,得到渲染的数组
  • 移动可视区域到滚动的距离位置

当有10000条数据,屏幕高度为500时, 子元素高度为50,那么可渲染区域应该渲染为10条数据, 整个列表高度为50 * 10000
JCDFaV.jpg

当我们发生滚动的时候,比如滚动了150px,那么我们可见区域的渲染列表就变成了下图 第4项到第13项了
JCDiV0.jpg

结构

<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
    })
}

效果

e0r5u-2k2gm.gif

可以看出,只渲染可视区域内的数据

设置上下缓存区

当滚动太块的时候,往下会有一段空白, 往上也有一段空白,那么这时候设置上下缓冲区可以解决此问题

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); 
} 

此时效果:

ayn6z-l262n.gif