无限滚动加载解决方案之虚拟滚动(下)

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是异步的,导致更新方法触发没跟上,所以需要对滚动事件再做一层的观察,判断是否当前的滚动高度已经更新过了,没更新则通过滚动结束后强行更新,当然,一定要记得对这个滚动事件加一层节流,减少事件触发频率。

  • 兼容问题

4774675cbe8823844a80ee17cbd14ecc.png

在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索引值来表达。

列表元素等高的场景 ,可以参考《无限滚动加载解决方案之虚拟滚动(上)》

✿  拓展阅读

fad716aeaeddd4ef7943751e952d6fca.png

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。

此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!

给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整28天,做了份“Java一线大厂高岗面试题解析合集:JAVA基础-中级-高级面试+SSM框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”

image

且除了单纯的刷题,也得需准备一本【JAVA进阶核心知识手册】:JVM、JAVA集合、JAVA多线程并发、JAVA基础、Spring 原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。

image

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!

给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整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)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值