Skip to content

不定高度的虚拟列表 #414

Open
@littleboyck

Description

@littleboyck

我自己写了一个不定高度的虚拟列表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>

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions