实现一个虚拟滚动,配合懒加载,滚动到相应位置再加载数据。
从外部进入,自动跳转到对应的位置,支持上下个数据切换。
从外部第三页跳转进来,自动定位到列表项的位置,可以滚动,自动加载相应区域的列表。
没加载过的列表会有小段时间白屏哈~
主要用这个: blog.csdn.net/qq_42268364…
其他: zhuanlan.zhihu.com/p/54327805 stackblitz.com/edit/react-…
还有一些算单个数据的偏移量的,我觉得还是算容器的比较正常
一个相对定位div包着两个绝对定位的div
一个高度为全列表高度,负责撑开滚动条
一个负责渲染的区域,由于滚动,要有一定的偏移,transform
<div
className="slide-container"
id="virtual"
style={{ position: "relative" }}
>
<div
style={{
height: allHeight,
position: "absolute",
width: "100%",
top: "0",
left: "0",
}}
id="virtual2"
/>
<div
style={{
transform: funcTran,
position: "absolute",
width: "100%",
height: "100%",
}}
>
<SlideList
isShowList={isShowList}
image_id={image_id}
image_list={showList}
/>
</div>
</div>
我这边每个列表项高度为101px,类似 写了一堆数据,我写了防抖是为了不去重复请求未加载的数据
- 列表总高度
listHeight = listData.length * itemSize - 可显示的列表项数
visibleCount = Math.ceil(screenHeight / itemSize) - 数据的起始索引
startIndex = Math.floor(scrollTop / itemSize) - 数据的结束索引
endIndex = startIndex + visibleCount - 列表显示数据为
visibleData = listData.slice(startIndex,endIndex) - 偏移量
startOffset = scrollTop - (scrollTop % itemSize);
layoutContent.addEventListener(
"scroll",
debounce(async (e) => {
// console.log("节流测试", e.timeStamp);
let { height, page_size } = this.state;
const { pageDataListMap, tableSource } = this.props;
let top = e.target.scrollTop;
let itemSum = Math.ceil(height / 101);
let start = Math.floor(top / 101);
let end = start + itemSum;
let offsetTop = top - (top % 101);
let funcTran = `translateY(${offsetTop}px)`;
this.setState({ funcTran });
这里列表显示数据有点难整,因为我没有全列表数据,所以不能那么搞,我这边请求一次20条数据。
我将请求过的列表数据存于pageDataListMap对象中,key为页码,value为列表,需要计算start和end所在页码,再截取其中的列表项出来显示。如果页码数据还没加载就要去加载,是await去执行,所以没数据时屏幕会空白一下。有能力的可以搞骨架屏。。
- 计算页码
//page_size:20,为啥+1,因为没0页。。
let page_index = Number(Math.floor(start / page_size) + 1);
let endPage = Number(Math.floor(end / page_size) + 1);
- 拼接数据,
start % page_size得到起始位置,拼数据,然后切割。+2是发现不加底部会留空我也不知道为啥,所以多搞点数据,展示列表就是showList
let startIndex = start % page_size;
showList = pageDataListMap.get(page_index).slice(startIndex);
if (endPage !== page_index) {
showList = showList.concat(pageDataListMap.get(endPage));
}
showList = showList?.splice(0, itemSum + 2);
这就是核心了
定位跳
要知道跳转列表项的索引,本项目是新开页面加载的,所以把第三页和50条每页的参数存local,然后在这边重新请求这个请求,根据url上显示的列表项id,找到索引,index * 101就是定位的高度
tableSource 是存 local的数据参数,有页码和每页条数
// 前两页 100条
let total = (tableSource.page_index - 1) * tableSource.page_size;
const { rows: image_list } = await getRequestFun(tableSource);
let index = 0;
image_list?.forEach((item, i) => {
if (item.id === image_id) {
index = i;
}
});
// 得出当前id在总列表中的序列
index += total;
let layoutContent = document.getElementById("virtual");
// 滚动滚动条使图片出现在当前位置
// 要判断是否滚动到最底不能再滚了,不能超到底
// scrollTop最大等于scrollHeight-clientHeight
let scrollTop = index * 101;
if (scrollTop + layoutContent.clientHeight > layoutContent.scrollHeight) {
layoutContent.scrollTop =
layoutContent.scrollHeight - layoutContent.clientHeight - 50;
} else {
layoutContent.scrollTop = index * 101;
}
跳转上下页按钮
点按钮不滚动列表,所以可能会有跳到未加载页码列表的情况,这边也要加载数据。
初始化和每次跳转后要判断有无上下个数据。若无要提前加载,实在没数据要禁用按钮。
每次切换列表条数时,需要遍历pageDataListMap,判断当前页数和索引,找出上一个和下一个的id,若没加载上一页和下一页,则请求列表加载
componentDidUpdate(prevProps, prevState, snapshot) {
。。。
pageDataListMap.forEach(async (item, key) => {
// 拿出列表数据
let ids = item?.map((i) => i.id);
// 如果包含当前列表项
if (ids?.includes(image_id)) {
// tableSource 是在local拿的数据
if (tableSource.page_index !== key) {
//更新local 新页码 必要的,因为不这样搞,有可能刷新后取不到定位不到位置
}
// 找出索引
let index = ids.indexOf(image_id);
// 如果是索引第一个 1
if (index === 0) {
// 而且是第一页,那没有上一页,上一页按钮禁用!
if (key === 1) {
this.setState({ preId: null, nextId: item[1]?.id || null });
} else {
// 不是第一页,拿出上一页,若每有请求过那就去请求。
//。取出上一页最后一个数据当前一页,以此类推,后面不多说了
let preArr = pageDataListMap.get(key - 1);
if (!preArr) {
let newTableSource = JSON.parse(JSON.stringify(tableSource));
newTableSource.page_index = key - 1;
newTableSource.page_size = page_size;
const { rows: image_list } = await getRequestFun(
newTableSource
);
pageDataListMap.set(key - 1, image_list);
this.props.fromListGetPageMap(pageDataListMap);
preArr = image_list;
}
this.setState({
preId: preArr[page_size - 1]?.id || null,
nextId: item[1]?.id || null,
});
}
} else if (index === page_size - 1) {
// 如果是索引最后一位
let nextArr = pageDataListMap.get(key + 1);
if (!nextArr) {
let newTableSource = JSON.parse(JSON.stringify(tableSource));
newTableSource.page_index = key + 1;
newTableSource.page_size = page_size;
const { rows: image_list } = await getRequestFun(newTableSource);
pageDataListMap.set(key + 1, image_list);
this.props.fromListGetPageMap(pageDataListMap);
nextArr = image_list;
}
// 没有下一页
this.setState({
preId: item[page_size - 2]?.id,
nextId: nextArr[0]?.id || null,
});
} else {
this.setState({
preId: item[index - 1]?.id,
nextId: item[index + 1]?.id,
});
}
}
});
}
没有上一页id的话就禁用按钮
<LeftOutlined
className={!preId ? "disabled-icon" : "action-icon"}
onClick={this.onPrevious}
/>
滚动快会重复请求
因为异步请求,同步滚的时候多次查当前位置页面数据没加载就去请求,异步没结束就多次查,就多次请求了
- 做了防抖,虽然会有白屏,但基本不会有重复请求的情况
- 请求时,将请求页码存变量数组里,每次请求去查数组里有没有这个页码再去请求
- 重复请求时 取消请求,案例:mp.weixin.qq.com/s?__biz=MjM…
可优化
- 白屏改骨架屏
- 跳转上下页按钮里遍历查找页面,没必要那么麻烦,在local存的页码 上下页也找一下就行