为什么要做长列表优化,让我们看一段代码的执行。
let now = Date.now();// 记录任务开始时间
let ul = document.getElementById('container'); //获取容器
let total = 100000;
for(let i = 0; i< total;i++) {
let li = document.createElement('li');
li.innerText = i;
ul.appendChild(li)
}
console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
console.log('总运行时间:',Date.now() - now);
},0)
可以看出当对十万条数据进行操作时,js运行时间166ms,但是最后渲染完成之后的时间确实4511ms.
图中可以看出:话费时间主要在:
Recalculate Style和Layout阶段,即样式计算和布局。
所以为了解决渲染阶段的性能瓶颈,我们主要通过减少渲染的时间。
第一种解决方案:采用时间分片。
1.setTimout处理。
let now = Date.now();// 记录任务开始时间
let ul = document.getElementById('container'); //获取容器
let total = 100000;
let pageSize = 30;
let currentPage = 1;
let endPage = Math.ceil(total/pageSize);
let currentTotal = total-pageSize*currentPage
function load(){
if(currentPage > endPage) {
return false;
}
let currentCount = Math.min(currentTotal, pageSize)
setTimeout(() => {
for(let i = 1;i<=currentCount;i++) {
let li = document.createElement('li');
li.innerText = currentPage + ':' + i;
ul.appendChild(li)
}
currentPage++;
currentTotal = total-pageSize*currentPage
load()
},0)
}
load()
但是,这种情况,在快速滑动情况下会导致白屏或者闪屏。原因就是关于js的执行进程和渲染进程的互斥。
关于setTimout和闪屏?
具体就是,浏览器内核的渲染进程:宏任务-渲染-宏任务-渲染这样…
1.因为setTimout的执行时间并不确定。在js执行机制中,setTimeout任务被放进事件队列中。只有主线程执行完毕才会检查事件队列中的任务。
2.刷新频率会受屏幕分辨率和屏幕尺寸的影响。
以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。
解决setTimout带来的问题,可以引入requestAnimationFrame
2.使用 requestAnimationFrame
requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。
//把上面的setTimout换掉
window.requestAnimationFrame(function(){
for(let i = 1;i<=currentCount;i++) {
let li = document.createElement('li');
li.innerText = currentPage + ':' + i;
ul.appendChild(li)
}
currentPage++;
currentTotal = total-pageSize*currentPage
load()
})
关于虚拟列表的实现
其是虚拟列表的主要特点就是减少dom数量,只需要改编list里每一项Item的值就好(即图中infinite-list-item的值)。这样加快dom树的构建。
<div id="infinite-list-container">
<!-- 用于占位 -->
<div id="infinite-list-phantom"></div>
<!-- 列表插入 -->
<div id="infinite-list">
</div>
</div>
<script>
(function () {
let total = 100;
let allData = [] //所有数据
for (let i = 0; i < total; i++) {
allData.push(i)
}
let $list = document.getElementById('infinite-list-container')
let $listPhantom = document.getElementById('infinite-list-phantom')
let $listContent = document.getElementById('infinite-list')
let itemSize = 100 //每一项的高度
let listHeiht = itemSize * total; //列表的总高度
let screenHeight = $list.clientHeight; //可见区域的高度
let visibleCount = Math.ceil(screenHeight / itemSize); //可见区域的列表项数
let start = 0; //起始位置
let end = start + visibleCount; //结束位置
let visivleData = allData.slice(start, Math.min(end, allData.length)) //可见区域的数据
let startOffset = 0 //偏移量
$listPhantom.style.height = allData.length * itemSize + 'px'
for (let i = 0; i < visivleData.length; i++) {
let div = document.createElement('div');
div.innerText = visivleData[i];
div.style.height = itemSize + 'px'
div.setAttribute('class', 'infinite-list-item')
$listContent.appendChild(div)
}
$list.addEventListener('scroll', () => {
console.log('sss')
let scrollTop = $list.scrollTop || document.documentElement.scrollTop;
console.log(scrollTop)
//此时的开始索引
start = Math.floor(scrollTop / itemSize);
//结束索引
end = start + visibleCount;
//偏移量
startOffset = scrollTop - scrollTop % itemSize
$listContent.style = `transform: translate3d(0,${start * itemSize}px,0)`
changeData();
})
function changeData() {
visivleData = allData.slice(start, Math.min(end, allData.length))
console.log(visivleData)
for (let i = 0; i < visivleData.length; i++) {
let div = document.getElementsByClassName('infinite-list-item')[i];
div.innerText = visivleData[i];
div.style.height = itemSize + 'px'
}
}
})()
</script>
<style>
#infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}
#infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
#infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.infinite-list-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>