JS+HTML实现列表动态无限滚动
问题
在HTML开发页面工程中,经常会遇到滚动列表-当实际需要显示的内容宽度或高度超过容器的宽度或高度时,设置CSS
overflow-x:auto;
overfow-y:auto;
当滚动列表中的内容比较少时,我们可以一次性加载所有的内容到列表容器中显示。
当滚动列表中的内容比较多,使用分页加载的方式逐步加载数据,2种方式
1.通过在列表的末尾添加一个标别元素indicator,和添加列表的scroll事件来监听indicator元素是否可见,如果可见那么提交请求加载下一页数据,append到列表内容的尾部。通过这个方式可以实现数据的无限加载,一直到数据取完。
2.在列表下部添加分页工具条,用户通过请求获取指定页的数据并且替换到当前列表里的内容
对于方式1对用户想对友好,加载过的数据不会重复加载,请求的资源少,但是当量多时,页面上的DOM元素量很不断增加,消耗的内存也会加大
方式2用户每翻一页都要重新请求数据,即使对于翻过看过的数据想要重新看也是如此,请求资源多,好处是页面DOM元素数量固定,占用内存资源少
那么有没有一种方式结合方式1和方式2的优点,摒弃缺点,融合优化下呢?
~~有!有方法~~
解决方案:
对当前列表的宽高固定,对列表包含的内容高度固定,列表滚动满足条件时动态删除或添加元素,保持元素数量的固定已经内容在列表可视区域的正确显示。
列表能够无限滚动,数据能够无限加载,DOM元素保持一定数量
先放出实现的代码吧
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="libs/jquery.js"></script>
<style>
.infinity-scroll{
width: 300px;
height: 500px;
position: absolute;
left:10px;
top:10px;
background-color: #ffffff;
box-sizing: border-box;
border:1px solid green;
overflow-x: hidden;
overflow-y: auto;
}
.main-content{
position: absolute;
left: 0px;
top: 0px;
width: 100%;
box-sizing: border-box;
background-color: #cccccc;
border: 1px solid #ffffff;
}
.item{
width: 100%;
height: 50px;
background-color: #ffffff;
}
.item:nth-child(2n+1){
background-color: green;
}
.scroll-wrapper{
width: 100%;
background-color: red;
height: 2000px;
}
.hide{
display: none;
}
</style>
<script>
$(function(){
var infinity_scroll_height;
var infinity_scroll_top;
var isLoading=false;
var lastMaxItemIndex=parseInt($(".item.last-item").last().attr("data-items-index"),10);
function appendNewItems(prev_main_content_height,scroll_top){
requestAnimationFrame(function(){
var documentFragment=document.createDocumentFragment();
var i,item;
var lastIndex=parseInt($(".item.last-item").attr("data-items-index"),10);
$(".item.last-item").removeClass("last-item");
for(i=0;i<10;i++){
lastIndex++;
item=$('<div class="item" data-items-index="0">2</div>');
$(item).attr("data-items-index",(lastIndex));
$(item).html(lastIndex+1);
if(i===9){
$(item).addClass("last-item");
}
documentFragment.appendChild($(item)[0]);
}
//更新此字段,作为是否 需要更新main-content的 height 的标志
//lastMaxItemIndex=Math.max(lastMaxItemIndex,lastIndex);
//同时removefirst-item后面的10个元素
$(".item").slice(0,10).remove();
$(".item").first().addClass("first-item");
var mainContentPaddingTop=$(".main-content").css("padding-top");
mainContentPaddingTop=parseFloat(mainContentPaddingTop.split("px")[0]||0);
$(".main-content").append(documentFragment);
if(lastMaxItemIndex<lastIndex){
$(".main-content").css({
"height":prev_main_content_height+10*50,
"padding-top":mainContentPaddingTop+10*50
});
lastMaxItemIndex=lastIndex;
}else{
$(".main-content").css({
"padding-top":mainContentPaddingTop+10*50
});
}
$('.infinity-scroll').off("scroll",infinity_scrollFun);
$('.infinity-scroll').scrollTop(scroll_top);
$('.infinity-scroll').on("scroll",infinity_scrollFun);
isLoading=false;
});
}
function restorePreItems(prev_main_content_height,scroll_top){
requestAnimationFrame(function(){
//删除底部的10个元素
var documentFragment=document.createDocumentFragment();
var i,item,mainContentPaddingTop;
var lastIndex=parseInt($(".item.first-item").attr("data-items-index"),10);
console.log(".item.first-item lastIndex%s",lastIndex);
if(lastIndex<=0){
isLoading=false;
return ;
}
$(".item.first-item").removeClass("first-item");
for(i=0;i<10;i++){
// lastIndex--;
item=$('<div class="item" data-items-index="0"></div>');
$(item).attr("data-items-index",(lastIndex-10+i));
$(item).html(lastIndex-10+i+1);
if(i===0){
$(item).addClass("first-item");
}
documentFragment.appendChild($(item)[0]);
}
//删除尾部的10个元素
$(".item.last-item").prevAll().slice(0,9).remove();
$(".item.last-item").remove();
$(".item").last().addClass("last-item");
mainContentPaddingTop=$(".main-content").css("padding-top");
mainContentPaddingTop=parseFloat(mainContentPaddingTop.split("px")[0]||0);
$(".main-content").css({
"padding-top":mainContentPaddingTop-10*50
});
$(".main-content").prepend(documentFragment);
$('.infinity-scroll').off("scroll",infinity_scrollFun);
$('.infinity-scroll').scrollTop(scroll_top);
$('.infinity-scroll').on("scroll",infinity_scrollFun);
isLoading=false;
//头部添加新的10个元素
});
}
function infinity_scrollFun(){
var last_item_top,first_item_top;
if(!infinity_scroll_height){
infinity_scroll_height=$(".infinity-scroll")[0].getBoundingClientRect().height;
infinity_scroll_top=$(".infinity-scroll")[0].getBoundingClientRect().top;
}
if(isLoading){
return;
}else{
last_item_top=$(".item.last-item")[0].getBoundingClientRect().top;
last_item_top=last_item_top-infinity_scroll_top;
}
console.log("~~~last_item_top=%s, infinity_scroll_height%s~~~",last_item_top,infinity_scroll_height);
if(last_item_top<=infinity_scroll_height){
isLoading=true;
appendNewItems($(".main-content")[0].getBoundingClientRect().height,$(this).scrollTop());
return;
}
first_item_top=$(".item.first-item")[0].getBoundingClientRect().top;
first_item_top=first_item_top-infinity_scroll_top;
console.log("~~first_item_top~~~%s",first_item_top);
if(first_item_top>=0){
restorePreItems($(".main-content")[0].getBoundingClientRect().height,$(this).scrollTop());
return;
}
console.log("~~~scrollTop~~~"+$(this).scrollTop());
}
$('.infinity-scroll').on("scroll",infinity_scrollFun);
});
</script>
</head>
<body>
<div class="infinity-scroll">
<div class="main-content">
<div class="item first-item" data-items-index="0">1</div>
<div class="item" data-items-index="1">2</div>
<div class="item" data-items-index="2">3</div>
<div class="item" data-items-index="3">4</div>
<div class="item" data-items-index="4">5</div>
<div class="item" data-items-index="5">6</div>
<div class="item" data-items-index="6">7</div>
<div class="item" data-items-index="7">8</div>
<div class="item" data-items-index="8">9</div>
<div class="item " data-items-index="9">10</div>
<div class="item " data-items-index="10">11</div>
<div class="item" data-items-index="11">12</div>
<div class="item" data-items-index="12">13</div>
<div class="item" data-items-index="13">14</div>
<div class="item" data-items-index="14">15</div>
<div class="item" data-items-index="15">16</div>
<div class="item" data-items-index="16">17</div>
<div class="item" data-items-index="17">18</div>
<div class="item" data-items-index="18">19</div>
<div class="item " data-items-index="19">20</div>
<div class="item " data-items-index="20">21</div>
<div class="item" data-items-index="21">22</div>
<div class="item" data-items-index="22">23</div>
<div class="item" data-items-index="23">24</div>
<div class="item" data-items-index="24">25</div>
<div class="item" data-items-index="25">26</div>
<div class="item" data-items-index="26">27</div>
<div class="item" data-items-index="27">28</div>
<div class="item" data-items-index="28">29</div>
<div class="item last-item" data-items-index="29">30</div>
</div>
</div>
</body>
</html>
HTML部分
.infinity-scroll
指定列表容器,CSS设置其水平不滚动,垂直方向自动滚动.main-content
为滚动内容组件,其高度随着实际内容的增多而加大.item
为滚动内容的单个项目元素.first-item
为当前放置在列表容器内的内容的第1个元素,同时为当前滚动到顶部的标识元素.last-item
为当前放置在列表容器内的内容的最后一个元素,同时为当前滚动到底部的标识元素
放3组元素作为初始的列表内容,为什么是3组呢?为了保证滚动的流畅性,当前列表窗口显示一组,卷起一组,下部隐藏一组
JS部分
$('.infinity-scroll').on("scroll",infinity_scrollFun);
为列表添加scroll事件监听infinity_scrollFun
infinity_scrollFun函数
计算列表容器的高度和离viewpoint顶部距离
if(!infinity_scroll_height){
infinity_scroll_height=$(".infinity-scroll")[0].getBoundingClientRect().height;
infinity_scroll_top=$(".infinity-scroll")[0].getBoundingClientRect().top;
}
计算.last-item
的位置,如果正在加载数据不执行任何动作
if(isLoading){
return;
}else{
last_item_top=$(".item.last-item")[0].getBoundingClientRect().top;
last_item_top=last_item_top-infinity_scroll_top;
}
如果.last-item
出现在列表窗口,那么加载新的数据
if(last_item_top<=infinity_scroll_height){
isLoading=true;
appendNewItems($(".main-content")[0].getBoundingClientRect().height,$(this).scrollTop());
return;
}
如果.first-item
出现在列表窗口,那么restore原来已经加载过的数据
first_item_top=$(".item.first-item")[0].getBoundingClientRect().top;
first_item_top=first_item_top-infinity_scroll_top;
console.log("~~first_item_top~~~%s",first_item_top);
if(first_item_top>=0){
restorePreItems($(".main-content")[0].getBoundingClientRect().height,$(this).scrollTop());
return;
}
appendNewItems函数
保持总体元素的个数,将.first-item
后的10个元素删除,添加新的内容到底部,并要保持内容的实际高度及显示在列表窗口的内容的位置
$(".item").slice(0,10).remove();
$(".item").first().addClass("first-item");
var mainContentPaddingTop=$(".main-content").css("padding-top");
mainContentPaddingTop=parseFloat(mainContentPaddingTop.split("px")[0]||0);
$(".main-content").append(documentFragment);
if(lastMaxItemIndex<lastIndex){
$(".main-content").css({
"height":prev_main_content_height+10*50,
"padding-top":mainContentPaddingTop+10*50
});
lastMaxItemIndex=lastIndex;
}else{
$(".main-content").css({
"padding-top":mainContentPaddingTop+10*50
});
}
//放置在设置scrollTop时页面滚动,先移除scroll事件监听,然后再添加监听
$('.infinity-scroll').off("scroll",infinity_scrollFun);
$('.infinity-scroll').scrollTop(scroll_top);
$('.infinity-scroll').on("scroll",infinity_scrollFun);
restorePreItems函数
功能和appendNewItems函数类似,只是删除底部的10个元素,添加10个元素到头部,并要保持内容的实际高度及显示在列表窗口的内容的位置
//删除尾部的10个元素
$(".item.last-item").prevAll().slice(0,9).remove();
$(".item.last-item").remove();
$(".item").last().addClass("last-item");
mainContentPaddingTop=$(".main-content").css("padding-top");
mainContentPaddingTop=parseFloat(mainContentPaddingTop.split("px")[0]||0);
$(".main-content").css({
"padding-top":mainContentPaddingTop-10*50
});
$(".main-content").prepend(documentFragment);
$('.infinity-scroll').off("scroll",infinity_scrollFun);
$('.infinity-scroll').scrollTop(scroll_top);
$('.infinity-scroll').on("scroll",infinity_scrollFun);
待改进的地方
上面的假设内容区的item的高度都是相同,并且每次加在的item数量都是相同的。如果内容item的高度是动态变化的;
同时没有做到页面DOM元素的复用,其实完全可以复用删除的元素作为将要添加的元素,只是变更其中的数据显示内容;
代码需要做相应的修改,就留给小伙伴们改进吧:)