列表虚加载
2022-11-15 本文已影响0人
欢西西西
- html
<div class="box" id="box">
<ul id="ul"></ul>
</div>
- css
.box {
height: 500px;
width: 400px;
border: 2px solid blue;
text-align: center;
overflow: auto;
padding-top: 20px;
margin-top: 50px;
}
#ul {
margin: 0;
list-style: none;
padding-left: 0;
}
#ul li {
margin: 10px 5px 10px;
padding: 5px 0 10px 0;
border: 1px solid #000;
}
- VirtualScroll
class VirtualScroll {
constructor({ scrollBox: box,
listBox: ul,
renderCount = 20, // 每次【最多】渲染N个,这里每批渲染的数量应该大于preDraw+每屏最多能放的卡片个数,不能设置太小
list, // 全部数据
onUptList, // 滚动时内部计算要渲染的数据传给onUptList
onReachEnd // 滚动到最底触发onReachEnd
}) {
this.preDraw = 5; // 在可视范围显示的第一个卡片前面再展示N个
this.btmConcatCount = 5; // 如果在触底后增加数据,则往下渲染N个
this._resetProp();
this.box = box;
this.ul = ul;
this.shift = ul.offsetTop;
this.renderCount = renderCount;
this.list = list;
this.onUptList = onUptList;
this.onReachEnd = onReachEnd;
this._bindEvent();
if (list.length) {
this._onScroll();
}
}
_resetProp() {
this.itemHeights = []; // 存每个卡片到顶部占的高度
this.startIndex = null;
this.endIndex = null; // 不包含
clearTimeout(this.scrollTimer);
this.scrollTimer = null;
}
// 计算要渲染的数据应从哪项开始
_getStartIndexOnScroll(scrollTop, arrHeight) {
if (!arrHeight.length) {
return 0;
}
let cur;
for (let i = 0, lth = arrHeight.length; i < lth; i++) {
if (scrollTop < arrHeight[i]) {
cur = i;
break;
}
}
return Math.max(cur - this.preDraw + 1, 0);
console.error('没找到合适的index');
console.log('scrollTop:', scrollTop, arrHeight);
}
// 计算应渲染的数据
_getDrawList(allData, startIndex, endIndex) {
let items = [];
while (startIndex < endIndex) {
let cur = allData[startIndex];
items.push(cur);
startIndex++;
}
return items;
}
_bindEvent() {
this.box.addEventListener('scroll', () => {
if (this.scrollTimer) {
return;
}
this.scrollTimer = setTimeout(() => {
this._onScroll();
clearTimeout(this.scrollTimer);
this.scrollTimer = null;
}, 250);
});
}
_isReachBtm() {
let box = this.box;
return box.scrollHeight - box.clientHeight - box.scrollTop < 50;
}
_onScroll() {
let scrollTop = this.box.scrollTop;
let box = this.box, list = this.list, itemHeights = this.itemHeights;
let start = this._getStartIndexOnScroll(scrollTop, itemHeights);
let calcCardsCount = itemHeights.length;
if (start === this.startIndex) {
if (calcCardsCount && this._isReachBtm() && calcCardsCount === list.length) { // 触底
return this.onReachEnd();
}
return;
}
this.startIndex = start;
this.endIndex = Math.min(list.length, start + this.renderCount);
this._updateDrawList();
calcCardsCount = itemHeights.length;
if (calcCardsCount && this._isReachBtm()) { // 触底
if (calcCardsCount === list.length) {
return this.onReachEnd();
}
}
}
// 计算完startIndex和endIndex来更新列表
_updateDrawList() {
let start = this.startIndex,
end = this.endIndex,
itemHeights = this.itemHeights,
calcCardsCount = itemHeights.length;
let topSpace = start > 0 ? itemHeights[start - 1] - this.shift + 'px' : 0,
bottomSpace = end < calcCardsCount ? itemHeights[calcCardsCount - 1] - itemHeights[end] + 'px' : 0;
this.ul.style.paddingTop = topSpace;
this.ul.style.paddingBottom = bottomSpace;
// 传给调用者自己更新列表,这里应该同步更新,因为下一步可能要存height
this.onUptList({
list: this._getDrawList(this.list, start, end),
topSpace,
bottomSpace
});
if (calcCardsCount < end) { // 之前没有存过height的再存
let i = start;
Array.from(this.ul.childNodes).forEach((node) => {
if (itemHeights[i] === undefined) {
itemHeights[i] = node.offsetHeight + node.offsetTop;
}
i++;
});
}
}
setData(list) { // 重新设置数据
this._resetProp();
this.list = list || [];
if (!this.list.length) {
return this.onUptList({
list: [], topSpace: 0, bottomSpace: 0
});
}
if (this.box.scrollTop > 0) {
this.box.scrollTo(0, 0);
} else {
this._onScroll();
}
}
addData(list) { // 拼接数据
if (!list || !list.length) {
return;
}
if (!this.list.length) {
this.list = list;
return this._onScroll();
}
this.list = this.list.concat(list);
if (this._isReachBtm()) {
this.endIndex = Math.min(this.list.length, this.startIndex + this.renderCount + this.btmConcatCount);
this._updateDrawList();
}
}
}
- 使用
// 生成假数据
function getMock(count) {
let data = [], minHeight = 30;
for (let i = 0; i < count; i++) {
data[i] = {
height: parseInt(minHeight + Math.random() * 100), // 随机行高
// height: 30,
color: `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`,
text: i + 1
}
}
return data;
}
// 如果每个li的上下margin不一样,这里滚动时也会抖动
let ul = document.getElementById('ul');
const v = new VirtualScroll({
scrollBox: document.getElementById('box'),
listBox: ul,
renderCount: 15,
list: getMock(50),
onUptList({ list }) {
ul.innerHTML = list.map(cur => `<li style='background-color: ${cur.color};line-height:${cur.height}px;'>
注意卡片是否抖动:${cur.text} </li>`).join('');
},
onReachEnd() {
console.log('加载下一页');
}
});