i++;
}
if (end + 10 > maxEndIndex) return;
updateState(newStart, newEnd, true);
}
// 向上滚动
if (entry.isIntersecting && entry.target.id === “top”) {
const newEnd = end === NODENUM ? NODENUM : (end - 10 > NODENUM ? end - 10 : NODENUM);
const newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0);
updateState(newStart, newEnd, false);
}
});
}
封装一个paddingTop和paddingBottom更新的函数,如果有当前的padding值,则取出渲染,如果没有,则保存下当前的paddingTop和paddingBottom。
const updateState = (newStart, newEnd, isDown) => {
if (config.current.setting) return;
config.current.syncStart = newStart;
if (start !== newStart || end !== newEnd) {
config.current.setting = true;
setStart(newStart);
setEnd(newEnd);
const page = ~~(newStart / 10) - 1;
if (isDown) { //向下
newStart !== 0 && !config.current.paddingTopArr[page] && (config.current.paddingTopArr[page] = $downAnchor.current.offsetTop);
// setPaddingTop(check ? config.current.paddingTopArr[page] : $downAnchor.current.offsetTop);
setPaddingTop(config.current.paddingTopArr[page]);
setPaddingBottom(config.current.paddingBottomArr[page] || 0);
}else { //向上
// const newPaddingBottom = $wrap.current.scrollHeight - $upAnchor.current.offsetTop;
const newPaddingBottom = $wrap.current.scrollHeight - $downAnchor.current.offsetTop;
newStart !== 0 && (config.current.paddingBottomArr[page] = newPaddingBottom);
setPaddingTop(config.current.paddingTopArr[page] || 0);
// setPaddingBottom(check ? config.current.paddingBottomArr[page] : newPaddingBottom);
setPaddingBottom(config.current.paddingBottomArr[page]);
}
setTimeout(() => {config.current.setting = false;},0);
}
}
在开发过程中发现,数据量非常大的情况下,快速滚动页面,由于api是异步的,导致更新方法触发没跟上,所以需要对滚动事件再做一层的观察,判断是否当前的滚动高度已经更新过了,没更新则通过滚动结束后强行更新,当然,一定要记得对这个滚动事件加一层节流,减少事件触发频率。
在safari的兼容问题,可以使用polyfill来解决。
// index.less
.item {
width: 100vw;
height: 240rpx;
border-bottom: 2rpx solid black;
}
.test {
height: 110rpx;
}
.container {
width: 100vw;
height: 100vh;
overflow: scroll;
-webkit-overflow-scrolling: touch;
}
// index.tsx
import { createElement, useEffect, useRef, useState } from “rax”;
import “./index.less”;
const arr = [];
// 模拟一共有2万条数据
for (let i = 0; i < 20000; i++) {
arr.push(i);
}
let i = 1;
// 默认第一屏取2页数据
const currentArr = arr.slice(0, 40), screenH = window.screen.height;
const NODENUM = 20;
function throttle(fn, wait) {
var timeout;
return function() {
var ctx = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
fn.apply(ctx, args);
}, wait);
};
}
function Index(props) {
const [start, setStart] = useState(0);
const [end, setEnd] = useState(NODENUM);
const [paddingTop, setPaddingTop] = useState(0);
const [paddingBottom, setPaddingBottom] = useState(0);
const [observer, setObserver] = useState(null);
const $bottomElement = useRef();
const $topElement = useRef();
const $downAnchor:any = useRef(); //定位paddingTop的距离
const $upAnchor:any = useRef(); //定位paddingBottom的距离
const $wrap:any = useRef(); //协助定位paddingBottom的距离
const container = useRef();
const config = useRef({
isRequesting: false,
paddingTopArr: [], //paddingTop数据栈
paddingBottomArr: [], //paddingBottom数据栈
preScrollTop: 0,
syncStart: 0,
setting: false,
});
const getReference = (index) => {
switch (index) {
case 0:
return $topElement;
case 5:
return $upAnchor;
case 10:
return $downAnchor;
case (NODENUM - 1):
return $bottomElement;
default:
return null;
}
}
const resetObservation = () => {
observer && observer.unobserve($bottomElement.current);
observer && observer.unobserve($topElement.current);
}
const intiateScrollObserver = () => {
const options = {
root: null,
rootMargin: ‘0px’,
threshold: 0.1
};
const Observer = new IntersectionObserver(callback, options);
if ($topElement.current) {
Observer.observe($topElement.current);
}
if ($bottomElement.current) {
Observer.observe($bottomElement.current);
}
setObserver(Observer);
}
const callback = (entries, observer) => {
entries.forEach((entry, index) => {
const listLength = currentArr.length;
// 向下滚动
if (entry.isIntersecting && entry.target.id === “bottom”) {
const maxStartIndex = listLength - 1 - NODENUM;
const maxEndIndex = listLength - 1;
const newStart = (end - 10) <= maxStartIndex ? end - 10 : maxStartIndex;
const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex;
if (newEnd + 10 >= maxEndIndex && !config.current.isRequesting && true) {
currentArr.push(…arr.slice(i * 40, (i + 1)* 40))
i++;
}
if (end + 10 > maxEndIndex) return;
updateState(newStart, newEnd, true);
}
// 向上滚动
if (entry.isIntersecting && entry.target.id === “top”) {
const newEnd = end === NODENUM ? NODENUM : (end - 10 > NODENUM ? end - 10 : NODENUM);
const newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0);
updateState(newStart, newEnd, false);
}
});
}
const updateState = (newStart, newEnd, isDown) => {
if (config.current.setting) return;
config.current.syncStart = newStart;
if (start !== newStart || end !== newEnd) {
config.current.setting = true;
setStart(newStart);
setEnd(newEnd);
const page = ~~(newStart / 10) - 1;
if (isDown) { //向下
newStart !== 0 && !config.current.paddingTopArr[page] && (config.current.paddingTopArr[page] = $downAnchor.current.offsetTop);
// setPaddingTop(check ? config.current.paddingTopArr[page] : $downAnchor.current.offsetTop);
setPaddingTop(config.current.paddingTopArr[page]);
setPaddingBottom(config.current.paddingBottomArr[page] || 0);
}else { //向上
// const newPaddingBottom = $wrap.current.scrollHeight - $upAnchor.current.offsetTop;
const newPaddingBottom = $wrap.current.scrollHeight - $downAnchor.current.offsetTop;
newStart !== 0 && (config.current.paddingBottomArr[page] = newPaddingBottom);
setPaddingTop(config.current.paddingTopArr[page] || 0);
// setPaddingBottom(check ? config.current.paddingBottomArr[page] : newPaddingBottom);
setPaddingBottom(config.current.paddingBottomArr[page]);
}
setTimeout(() => {config.current.setting = false;},0);
}
}
useEffect(() => {
document.getElementsByClassName(‘container’)[0].addEventListener(‘scroll’, scrollEventListner);
}, [])
useEffect(() => {
resetObservation();
intiateScrollObserver();
}, [end, currentArr])
const scrollEventListner = throttle(function (event) {
const scrollTop = document.getElementsByClassName(‘container’)[0].scrollTop;
let index = config.current.paddingTopArr.findIndex(e => e > scrollTop);
index = index <= 0 ? 0 : index;
const len = config.current.paddingTopArr.length;
len && (config.current.paddingTopArr[len - 1] < scrollTop) && (index = len);
const newStart = index * 10;
const newEnd = index * 10 + NODENUM;
if (newStart === config.current.syncStart) {
config.current.preScrollTop = scrollTop;
return;
}
updateState(newStart, newEnd, scrollTop > config.current.preScrollTop); //true为往下滚动 false为往上滚动
config.current.preScrollTop = scrollTop;
}, 100);
return (
{currentArr.slice(start, end).map((item, index) => {
const refVal = getReference(index);
const id = index === 0 ? ‘top’ : (index === (NODENUM - 1) ? ‘bottom’ : ‘’);
const classValue = index % 4 === 0 ? ‘test’ : ‘’
return
{item}
})}
);
}
export default Index;
总结
定高和不定高2类虚拟滚动,适合用户消费海量数据的场景,尤其是社交媒体这类数据消费。而像通讯录,好友列表这类数据量单一且不会特别多的情况,还是用传统的滚动体验会更好。技术选型就按照自己业务的实际需要去出发选择,千万不要强行套用。
关于懒加载这块的问题,需要根据自己的实际情况来判断是否让元素强行更新,在react下,默认的元素key应该用元素遍历的map索引值来表达。
列表元素等高的场景 ,可以参考《无限滚动加载解决方案之虚拟滚动(上)》
✿ 拓展阅读
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://i-blog.csdnimg.cn/blog_migrate/15bc399840d6cb9a5aaa55b6caeea7a5.jpeg)
总结
面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。
此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!
给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”
且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!
给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”
[外链图片转存中…(img-UHnurNm2-1713728260516)]
且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。
[外链图片转存中…(img-4OoKoY7H-1713728260516)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!