5

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元素的复用,其实完全可以复用删除的元素作为将要添加的元素,只是变更其中的数据显示内容;

代码需要做相应的修改,就留给小伙伴们改进吧:)


kikong
19.1k 声望902 粉丝

java/javascript/nodejs,制造更好的轮子~~