vue-virtual-scroll-list
vue-virtual-scroll-list copied to clipboard
不定高度的虚拟列表
我自己写了一个不定高度的虚拟列表demo,我给了一定的缓存区,当滚轮移动过快时,仍然会存在白屏问题,能不能帮我解决一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>不定高度的虚拟列表</title>
</head>
<body>
<style>
.list {
height: 400px;
width: 300px;
outline: 1px solid seagreen;
overflow-x: hidden;
}
.list-item {
outline: 1px solid red;
outline-offset:-2px;
background-color: #fff;
}
</style>
<div class="list">
<div class="list-inner"></div>
</div>
<script>
const throttle = (callback) => {
let isThrottled = false;
return (...args)=> {
if (isThrottled) return;
callback.apply(this, args);
isThrottled = true;
requestAnimationFrame(() => {
isThrottled = false;
});
}
}
const randomIncludes = (min, max) => {
return Math.floor(Math.random()*(max - min + 1) + min);
}
const clientHeight = 400;
const listEl = document.querySelector('.list');
const listInner = document.querySelector('.list-inner');
function initAutoSizeVirtualList(props) {
const cache = [];
//window.cache = cache;
const { listEl, listInner, minSize = 30, clientHeight, items } = props;
// 默认情况下可见数量
const viewCount = Math.ceil(clientHeight / minSize);
// 缓存区数量
const bufferSize = Math.floor(viewCount / 2);
listEl.style.cssText += `height:${clientHeight}px;overflow-x: hidden`;
const findItemIndex = (startIndex, scrollTop) => {
scrollTop === undefined && (
scrollTop = startIndex,
startIndex = 0
)
let totalSize = 0;
for(let i = startIndex; i < cache.length; i++) {
totalSize += cache[i].height;
if(totalSize >= scrollTop || i == cache.length - 1) {
return i;
}
}
return startIndex;
}
// 更新每个item的位置信息
const upCellMeasure = () => {
const listItems = listInner.querySelectorAll('.list-item');
if(listItems.length === 0){return}
const lastIndex = +listItems[listItems.length - 1].dataset.index;
[...listItems].forEach((listItem) => {
const rectBox = listItem.getBoundingClientRect();
const index = listItem.dataset.index;
const prevItem = cache[index-1];
const top = prevItem ? prevItem.top + prevItem.height : 0;
Object.assign(cache[index], {
height: rectBox.height,
top,
bottom: top + rectBox.height
});
});
// 切记一定要更新未渲染的listItem的top值
for(let i = lastIndex+1; i < cache.length; i++) {
const prevItem = cache[i-1];
const top = prevItem ? prevItem.top + prevItem.height : 0;
Object.assign(cache[i], {
top,
bottom: top + cache[i].height
});
}
}
const getTotalSize = () => {
return cache[cache.length - 1].bottom;
}
const getStartOffset = (startIndex) => {
return cache[startIndex].top;
}
const getEndOffset = (endIndex) => {
return cache[endIndex].bottom;
}
// 缓存位置信息
items.forEach((item, i) => {
cache.push({
index:i,
height: minSize,
top: minSize * i,
bottom: minSize * i + minSize
});
});
return function autoSizeVirtualList(renderItem, callback) {
const startIndex = findItemIndex(listEl.scrollTop);
const endIndex = startIndex + viewCount;
// const endIndex = findItemIndex(startIndex, clientHeight);
const startBufferIndex = Math.max(0, startIndex - bufferSize);
const endBufferIndex = Math.min(items.length-1, endIndex + bufferSize);
const renderItems = [];
for(let i = startBufferIndex; i <= endBufferIndex; i++) {
renderItems.push(renderItem(items[i], i, cache[i]))
}
upCellMeasure();
const startOffset = getStartOffset(startBufferIndex);
const endOffset = getTotalSize() - getEndOffset(endBufferIndex);
listInner.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);
return renderItems;
}
}
// 模拟1万条数据
const count = 10000;
const items = Array.from({ length: count }).map((item, i) => ({ name: `item ${(i+1)}`, height: randomIncludes(40, 120) }) );
const autoSizeVirtualList = initAutoSizeVirtualList({ listEl, listInner, clientHeight, items });
document.addEventListener('DOMContentLoaded', () => {
const renderItems = autoSizeVirtualList((item, i) => {
return `<div class="list-item" data-index="${i}" style="height:${item.height}px">${item.name}</div>`
});
listInner.innerHTML = renderItems.join('');
});
listEl.addEventListener('scroll', throttle(() => {
const renderItems = autoSizeVirtualList((item, i) => {
return `<div class="list-item" data-index="${i}" style="height:${item.height}px">${item.name}</div>`
});
listInner.innerHTML = renderItems.join('');
}));
</script>
</body>
</html>
我已经发现了问题,就是需要在每次开始时,就需要跟新listItem的数据,下面是更新后的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>不定高度的虚拟列表</title>
</head>
<body>
<style>
.list {
height: 400px;
width: 300px;
outline: 1px solid seagreen;
overflow-x: hidden;
}
.list-item {
outline: 1px solid red;
outline-offset:-2px;
background-color: #fff;
}
</style>
<div class="list">
<div class="list-inner"></div>
</div>
<script>
function throttle(callback) {
let requestId;
return (...args) => {
if (requestId) {return}
requestId = requestAnimationFrame(() => {
callback.apply(this, args);
requestId = null;
});
};
}
const randomIncludes = (min, max) => {
return Math.floor(Math.random()*(max - min + 1) + min);
}
const clientHeight = 400;
const listEl = document.querySelector('.list');
const listInner = document.querySelector('.list-inner');
function initAutoSizeVirtualList(props) {
const cache = [];
// window.cache = cache;
const { listEl, listInner, minSize = 30, clientHeight, items } = props;
// 默认情况下可见数量
const viewCount = Math.ceil(clientHeight / minSize);
// 缓存区数量
const bufferSize = 5;
listEl.style.cssText += `height:${clientHeight}px;overflow-x: hidden`;
// const findItemIndex = (startIndex, scrollTop) => {
// scrollTop === undefined && (
// scrollTop = startIndex,
// startIndex = 0
// )
// let totalSize = 0;
// for(let i = startIndex; i < cache.length; i++) {
// totalSize += cache[i].height;
// if(totalSize >= scrollTop || i == cache.length - 1) {
// return i;
// }
// }
// return startIndex;
// }
// 二分查询优化
const findItemIndex = (startIndex, scrollTop) => {
scrollTop === undefined && (
scrollTop = startIndex,
startIndex = 0
);
let low = startIndex;
let high = cache.length - 1;
const { top: startTop, bottom: startBottom } = cache[startIndex];
while(low <= high) {
const mid = Math.floor((low + high) / 2);
const { top: midTop, bottom: midBottom } = cache[mid];
const top = midTop - startTop;
const bottom = midBottom - startBottom;
if (scrollTop >= top && scrollTop < bottom) {
high = mid;
break;
} else if (scrollTop >= bottom) {
low = mid + 1;
} else if (scrollTop < top) {
high = mid - 1;
}
}
return high;
}
// 更新每个item的位置信息
const upCellMeasure = () => {
const listItems = listInner.querySelectorAll('.list-item');
if(listItems.length === 0){return}
const lastIndex = +listItems[listItems.length - 1].dataset.index;
[...listItems].forEach((listItem) => {
const rectBox = listItem.getBoundingClientRect();
const index = listItem.dataset.index;
const prevItem = cache[index-1];
const top = prevItem ? prevItem.top + prevItem.height : 0;
Object.assign(cache[index], {
height: rectBox.height,
top,
bottom: top + rectBox.height
});
});
// 切记一定要更新未渲染的listItem的top值
for(let i = lastIndex+1; i < cache.length; i++) {
const prevItem = cache[i-1];
const top = prevItem ? prevItem.top + prevItem.height : 0;
Object.assign(cache[i], {
top,
bottom: top + cache[i].height
});
}
}
const getTotalSize = () => {
return cache[cache.length - 1].bottom;
}
const getStartOffset = (startIndex) => {
return cache[startIndex].top;
}
const getEndOffset = (endIndex) => {
return cache[endIndex].bottom;
}
// 缓存位置信息
items.forEach((item, i) => {
cache.push({
index:i,
height: minSize,
top: minSize * i,
bottom: minSize * i + minSize,
isUpdate: false
});
});
return function autoSizeVirtualList(renderItem) {
// 在一开始就需要更新item的位置信息,否则,会出现白屏问题
upCellMeasure();
const startIndex = findItemIndex(listEl.scrollTop);
const endIndex = startIndex + viewCount;
// console.log(startIndex, findItemIndex(startIndex, clientHeight))
const startBufferIndex = Math.max(0, startIndex - bufferSize);
const endBufferIndex = Math.min(items.length-1, endIndex + bufferSize);
const renderItems = [];
for(let i = startBufferIndex; i <= endBufferIndex; i++) {
renderItems.push(renderItem(items[i], cache[i]))
}
// 在此处更新,顶部会有白屏
// upCellMeasure();
const startOffset = getStartOffset(startBufferIndex);
const endOffset = getTotalSize() - getEndOffset(endBufferIndex);
listInner.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);
return renderItems;
}
}
// 模拟10万条数据
const count = 100000;
const items = Array.from({ length: count }).map((item, i) => ({ name: `item ${(i+1)}`, height: randomIncludes(40, 120) }) );
const autoSizeVirtualList = initAutoSizeVirtualList({ listEl, listInner, clientHeight, items });
document.addEventListener('DOMContentLoaded', () => {
const renderItems = autoSizeVirtualList((item, rectBox) => {
return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`
});
listInner.innerHTML = renderItems.join('');
});
listEl.addEventListener('scroll', throttle(() => {
const renderItems = autoSizeVirtualList((item, rectBox) => {
return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`
});
listInner.innerHTML = renderItems.join('');
}));
</script>
</body>
</html>