tangbc / vue-virtual-scroll-list

⚡️A vue component support big amount data list with high render performance and efficient.

Home Page:https://tangbc.github.io/vue-virtual-scroll-list

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

不定高度的虚拟列表

littleboyck opened this issue · comments

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