虚拟列表-实现教程-全网最清晰?
问题一、如果给定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数量固定,那么你实现的就是虚拟列表。如果在阅读过程中有发现问题,欢迎评论区评论,下次再见啦。
参考
转载自:https://juejin.cn/post/7198782832176169015