Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

不定高度的虚拟列表 #414

Open
littleboyck opened this issue Jul 27, 2023 · 1 comment
Open

不定高度的虚拟列表 #414

littleboyck opened this issue Jul 27, 2023 · 1 comment
Labels

Comments

@littleboyck
Copy link

littleboyck commented Jul 27, 2023

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

littleboyck commented Jul 28, 2023

我已经发现了问题,就是需要在每次开始时,就需要跟新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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant