海量列表渲染问题
相信任何一个前端开发者都遇到过这种问题,我们需要将大量的数据写入列表,然后再将它们渲染到浏览器上。毫无疑问,当数据量不大的时候,我们不需要做什么处理,一个简单的map直接就搞定了。但是,当数据量达到几万,几十万,甚至几百万,这时候如果你在直接使用一个map的话,估计浏览器会直接崩掉。这是为什么,因为列表中一个元素搞不好的就有多个div,那几万个元素就有几十万个div,浏览器一下子解析几十万个div,大几率会直接崩掉。说实话,这个问题一直都是很令人头疼的,因为虽然有解决方法,但是应用场景的不同很有可能会不适用,今天我们主要来讲几个最常见的解决方案。
分页
分页这种解决方法,可以说是能解决所有海量列表渲染的问题。它的原理就是将数据分成一页页的,所以说每一页只展示了少量的数据,不会存在卡顿的问题。所以这个方案在各大网站上都能看到。但是对我个人而言,我不是很喜欢这种方法,首先从交互上来说,用户可能更倾向于往下滑,一直滑到底。还有一点就是,你点击其他页码的时候,上一个页码的数据就无法保持了,当你想再查看上一个页面数据的时候,还得重新点回去,还要继续往下翻到刚才看的位置,我一直觉得这个操作很反人类。但是仍然有很多网站使用这种方式,所以说肯定是有其自身的优势的,大家可以根据自己的需求去选择最适合自己的。
滚动加载
由于上面我们分析了分页的缺点,那么使用滚动加载就更好了吧。其实不然,滚动加载存在着一个致命的问题,当我们一直往下滚动的时候会出现一个问题,那就是列表越来越大,dom越来越多,当滚动到一定程度时,浏览器也会出现卡顿的问题,然后直接崩掉。这其实跟直接渲染差不太多,只是说它是逐渐增大的。所以说,这种方案也会只适用于中小数据的场景下。
可视区加载
由于滚动加载方案的不适用性,所以说就有了另外一个方案。列表虽然很大,但是在我们视觉范围内的元素只有那么几个。所以说,我们只需要渲染在我们视觉范围内的元素。这样,无论列表有多大,对我们造不成任何影响。竟然有了思路,我们就想办法把它实现出来,下面是我的第一种方案:
- 获取可视区高度,计算开始渲染的位置和结束的位置
- 给container绑定scroll事件,根据scrollTops随时更新渲染的开始和结束位置
- 不在可视区范围内的元素统统设置display:none
看起来没有什么问题,但是实现起来的时候就出现了一个特别难搞的问题,进度条的位置无法保持,因为我们给元素设置display:none的话进度条就会回到起点,尝试过各种方法,进度条位置依然无法保持,所以第一个方案失败。
第一种方案最致命的地方就是进度条位置无法保持,我们直接操作container内的元素,会导致container的高度一直是在变化的,所以进度条根本无法控制。鉴于以上原因,我想除了另外一种方案,当我们对元素进行操作的时候不会影响container的高度,以下是我的第二种方案:
- 加入一个container,获取container的高度,这个高度就是可视区的高度
- 获取子元素的高度,计算一屏能够容纳下的元素个数
- 创建一个set-height元素,它的作用是把container的scroll-height撑起来
- 创建一个list放置生成的元素,list设置为absolute,对它进行操作不会对container造成影响
- 滚动时的操作:
a:获取container的scrollTop,计算list的位置,保证list覆盖container的可视区域。
b: 通过scrollTop计算出当前可视区域应该显示的元素位置,然后根据位置更新渲染结果
c: 滚动的时候,list会跟着containe一起往上滚动,所以说这个时候根据translateY('往下移动的距离')来调整list的位置,让它永远保持在可视区域,这个移动的位置可以根据scrollTop计算。实践发现这个方案可行,以下是源码片段:
<body>
<div class="container">
<div class="list"></div> //list设置为absolute
<div class="scroll-height-box"></div> //撑开container的滚动高度
<div class="back-top"></div> //回顶
</div>
<script>
var array=[];
var eleH=150; //元素高度
var eleW=200; //元素宽度
var count=1250;
for(var i=0;i<10000;i++){
array[i]=i
}
var box=document.querySelector('.out-box');
var container=document.querySelector('.container');
var list=document.querySelector('.list');
var heightBox=document.querySelector('.scroll-height-box');
var backBtn=document.querySelector('.back-top');
backBtn.onclick=function(){
container.scrollTop=0;
}
window.onload=function(e){
var visibleHeight=container.offsetHeight;
var visibleCount=Math.floor(visibleHeight/eleH); //可视区域渲染元素的个数
var rowCount=Math.floor(container.offsetWidth/(eleW+16));
list.style.height=visibleHeight+'px'; //设置list的高度为可视高度
heightBox.style.height=(eleH)*count+'px'; //撑开的高度为元素个数乘以高度
list.innerHTML=renderNode(0,visibleCount*rowCount+7);
container.addEventListener('scroll',() => {
if(container.scrollTop>1200){ //回顶操作
backBtn.style.visibility='visible';
}else {
backBtn.style.visibility='hidden';
}
list.style.transform=`translateY(${container.scrollTop-container.scrollTop%eleH}px)` //调整list的位置保持永远在可视区域
var startIndex=Math.floor(container.scrollTop/eleH); //滚动时不断调整元素的开始位置
list.innerHTML='';
list.innerHTML=renderNode(startIndex*rowCount,startIndex*rowCount+visibleCount*rowCount+7);
})
}
function renderNode(startIndex,stopIndex){
console.log(startIndex,stopIndex)
var ele='';
for(var i=startIndex;i<=stopIndex;i++){
ele+=`<div class="test-box">${array[i]}</div>`
}
return ele
}
大家要注意一点的是,当给元素绑定scroll事件的时候,一定要给该元素设置高度,并且overflow设置为auto或者scroll,其次就是它的子元素的高度一定要比它高,只有这样才能撑开它的滚动高度。这两个条件一个不符合的话,scroll事件就不会被触发。今天就先到这里了,后面我会专门封装一个react的可视滚动组件,还望大家多多支持!!