likes
comments
collection
share

虚拟列表-实现教程-全网最清晰?

作者站长头像
站长
· 阅读数 16

问题一、如果给定100条数据,你将如何渲染它?

100条数据的渲染量不大,可以直接渲染,react伪代码如下:

let originData = new Array(100).fill(1)

originData.map((item,index) => {
    return <div>{index}</div>
})

但如果是1000条呢?那么此时相信所有人都不会去直接渲染。

问题二、如何优化渲染100条数据?

优化渲染有很多种方式,这里我们采用滚动时固定dom数量的策略。对这种策略的定义如下:

对大数据量(假定数据总量为m)渲染的情况,我们采用分批渲染的方式,一次只渲染n个,Math.ceil(m / n) 次全部渲染完毕。

1、渲染过程讲解

虚拟列表-实现教程-全网最清晰?

下面我们分别来解读下上图中的2个状态:

一、状态1

数据总量是100,初始化页面上只展示5个,绿色框代表父容器,5条数据的高度 > 父容器的高度,所以父容器产生了滚动条。

二、状态2

由状态1向下滚动而来。此时有3个问题:

  • a、状态1向下滚动了多少条数据才变为的状态2?
  • b、状态2的dom数量为什么是11条?
  • c、状态2-5的dom数量为什么在递增?

对于问题a,在这里我们认为是向下完整的滚动了 1条 数据得到的状态2。那么在这里我们定义一个变量scrolledDataCount(伪代码如下),用于记录滚动了多少条完整的数据项。

let scrolledDataCount = Math.floor(滚动过的距离 / 每个数据项的高度);

对于问题b,我们首先要明确的是,父容器在滚动的时候,数据是动态变换的,所以我们需要截取数据来达到这样的效果。在截取数据之前,我们还要考虑的一个问题就是截取数据的时机,那么下面我们来逐个击破。

三、截取数据的时机

截取数据的时机大家自行决定,这里我们规定截取数据的时机如下:

1、如果此时dom数量小于15,那么此时就需要不断的push 2、如果dom数量 >= 15,那么此时就需要不断的update数据,并且保证dom数量 == 15

四、截取数据的逻辑

这里面我们使用slice来截取数组,所以我们需要知道startIndex与endIndex。伪代码如下:

    //  滚动过的数据量 - 5 < 0 对应的时机是push
    //  滚动过的数据量 - 5 >= 0 对应的时机是update
    let startIndex = 滚动过的数据量 - 5 < 0 ? 0 : 滚动过的数据量 - 5;
    let endIndex = 每次要加载的数据量(5) + 每页展示的数据量(5) + 滚动过的数据量(在递增或者递减);

由于endIndex随时都在变,并且滚动过的数据量在递增,所以状态2-5的dom数量在递增。问题c到这里就解决了。

至于问题b,我们可以来分析一下,父容器向下滚动1条数据,此时的startIndex == 0,endIndex = 5 + 5 + 1, 所以此时的dom数量是11(data.slice(0, 11).length)。

2、示例代码

import React from 'react';

export default class Practice extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            allData: new Array(200).fill(1).map( (item, index) => ({ name: index }) ),
            dataItemHeight: 200, // 每个任务的高度
            onePageViewAllDataCount: 5, // 1页展示多少个任务数量
            viewDataObject: {
                startIndex: 0,
                endIndex: 5
            },
            fatherComponent: React.createRef(),
            onePageViewAllData: [], // 一页展示的所有数据
        }
    }
    
    // 组件滚动
    componentScroll = (event) => {
        const { fatherComponent } = this.state;
        let scrolledDataCount = 0; // 滚动过的完整数据数量
        if (fatherComponent.current){
            scrolledDataCount = Math.floor(Number(fatherComponent.current?.scrollTop || 0) / 200);
            let startSize = scrolledDataCount - 5;
            let endSize = 5 + scrolledDataCount + 5;
            this.setState(state => {
                return {
                    ...state,
                    viewDataObject: {
                        startIndex: startSize < 0 ? 0 : startSize,
                        endIndex: endSize
                    }
                }
            }, () => {
                this.setState(state => {
                    return {
                        ...state,
                        onePageViewAllData: state.allData.slice(state.viewDataObject.startIndex, state.viewDataObject.endIndex)
                    }
                });
            })
        }
    }

    // 初始化,截取5个任务
    componentDidMount(){
        this.setState(state => {
            return {
                ...state,
                onePageViewAllData: state.allData.slice(state.viewDataObject.startIndex, state.viewDataObject.endIndex)
            }
        })
    }

    render(){
        const { allData, dataItemHeight, fatherComponent, onePageViewAllData } = this.state;
        return <div className = 'virtually-box'>
            <div className = 'virtually-component' ref={fatherComponent} onScroll={this.componentScroll}>
                <div className = 'virtually-component-data'>
                    {
                        onePageViewAllData.map(item => {
                            return <div className = 'virtually-component-data-item' style={{ height: `${dataItemHeight}px` }}>
                                {item.name}
                            </div>
                        })
                    }
                </div>
            </div>
        </div>
    }
}

3、示例效果

虚拟列表-实现教程-全网最清晰?

问题三、如何获取触底时机?

相信这个大家应该耳熟能详了,公式如下:

if (father.scrollTop + father.clientHeight >= father.scrollHeight){
    return true;
}
return false;

最后总结

1、本篇文章只是实现了最简单、最基础的虚拟列表(它的玩法有很多)。欢迎大神们在评论区里扩展。

2、其实虚拟列表的本质就是固定dom数量, 只要你能够分批渲染大数据量的list,并且能够保证dom数量固定,那么你实现的就是虚拟列表。如果在阅读过程中有发现问题,欢迎评论区评论,下次再见啦。

参考

「前端进阶」高性能渲染十万条数据(虚拟列表)