高性能渲染大量数据
对于大量数据的渲染,可以采用时间分片和虚拟列表的处理方式
时间分片
简单点理解就是把数据拆成很多份,分批地来进行渲染。页面卡顿是因为同时渲染大量 DOM 所引起的可使用时间分片。
方式一:setTimeout
缺点:但是当我们快速滚动页面的时候,会发现页面出现闪屏或白屏的现象
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
setTimeout(()=>{
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount,curIndex + pageCount)
},0)
}
loop(total,index);
方式二:requestAnimationFrame + DocumentFragment
//需要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每页多少条
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
let fragment = document.createDocumentFragment();
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);
虚拟列表
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
1、计算当前可视区域起始数据索引(startIndex)
2、计算当前可视区域结束数据索引(endIndex)
3、计算当前可视区域的数据,并渲染到页面中
4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上
<!-- 虚拟列表 -->
<div class="box">
<!-- 内容真实高度 即200条数据的高度-->
<div class="content-area" id="contentArea">
<ul></ul>
</div>
</div>
let el = document.querySelector(".box");
let itemHeight = 110; //每个元素的高度(li的高度100 + marginBottom 10)
let pageSize = Math.ceil(el.clientHeight / itemHeight); // 获取一个滚动屏最大可容纳子元素个数(向上取整)
let data = []; //mock数据
let startIndex = 0; //可视区第一行下标
let endIndex = pageSize; //可视区最后一行下标
// 初始化模拟数据
let getData = () => {
for (let i = 0; i < 200; i++) {
data.push({
content: `我是显示的内容${
i + 1}`,
});
}
};
// 加载数据并插入到dom页面
let loadData = () => {
let html = "";
let sliceData = data.slice(startIndex, endIndex);
for (let i = 0; i < sliceData.length; i++) {
html += `
<li class="item">
<p>${
sliceData[i].content}</p>
</li>`;
}
el.querySelector("#contentArea ul").innerHTML = html;
};
// 更新DOM
let updateHtml = () => {
let sliceData = data.slice(startIndex, endIndex);
let itemAll = el.querySelectorAll(".item");
for (let i = 0; i < sliceData.length; i++) {
itemAll[i].querySelector("p").innerHTML = sliceData[i].content;
}
};
// 滑动监听
el.addEventListener("scroll", function () {
let scrollTop = el.scrollTop; // 滚动高度
startIndex = Math.ceil(scrollTop / itemHeight); // 重新计算开始的下标,div顶部卷起来的长度除以列表元素的高度
endIndex = startIndex + pageSize;
updateHtml(); // 重新更新dom
el.querySelector("#contentArea ul").style.transform =
"translateY(" + startIndex * itemHeight + "px)";
});
let init = () => {
getData();
loadData();
document.getElementById("contentArea").style.height =
itemHeight * data.length + "px"; // 占位dom的高度
};
// 页面初始化调用
init();
懒加载
不多介绍,一句话解释:最开始不渲染所有数据,只展示视图上可见的数据,当滚动到页面底部时,加载更多数据
实现原理:通过监听父级元素的 scroll 事件,当然也可以通过 IntersectionObserver 或 getBoundingClientRect 等 API 实现
但 scroll 事件会频繁触发,所以需要手写节流;滚动元素内有大量 DOM ,容易造成卡顿,建议使用 IntersectionObserver