首页 > 编程语言 >原生JS实现一个不固定高度的虚拟列表核心算法

原生JS实现一个不固定高度的虚拟列表核心算法

时间:2023-08-08 13:01:01浏览次数:37  
标签:原生 const top cache 列表 height let return JS

<!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>
        // 快速移动滚动条,中间未渲染部分,导致渲染后高度偏移差问题
        // 参考链接:https://lkangd.com/post/virtual-infinite-scroll/
        // const throttle = (callback) => {
        //     let isThrottled = false;
        //     return (...args)=> {
        //         if (isThrottled) return;
        //             callback.apply(this, args);
        //         isThrottled = true;
        //         requestAnimationFrame(() => {
        //             isThrottled = false;
        //         });
        //     }
        // }

        // function run(task, taskEndCallback) {
        //     let oldDate = Date.now();
        //     requestAnimationFrame(() => {
        //         let now = Date.now();
        //         console.log(now - oldDate)
        //         if(now - oldDate <= 16.5) {
        //             const result = task();
        //             taskEndCallback(result);
        //         }else {
        //             run(task, render);
        //         }
        //     })
        // }

        // function debounce(callback) {
        //     let timerId;
        //     return function() {
        //         if (timerId) {
        //             cancelAnimationFrame(timerId);
        //         }
        //         timerId = requestAnimationFrame(() => {
        //             callback.apply(this, arguments);
        //         });
        //     };
        // }

        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;
            let oldFirstIndex = 0;
            const { listEl, listInner, minSize = 30, clientHeight, items } = props;
            // 默认情况下可见数量
            const viewCount = Math.ceil(clientHeight / minSize);
            // 缓存区数量
            const bufferSize = 6;
            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 = (scrollTop) => {
                let low = 0; 
                let high = cache.length - 1;
                while(low <= high) {
                    const mid = Math.floor((low + high) / 2);
                    const { top, bottom } = cache[mid];
                    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 firstItem = listItems[0];
                const firstIndex = +firstItem.dataset.index;
                const lastIndex = +listItems[listItems.length - 1].dataset.index;
                // 解决向上缓慢滚动时,高度存在的偏移差问题,非常重要
                if(firstIndex < oldFirstIndex && !cache[firstIndex].isUpdate) {
                    const dHeight = firstItem.getBoundingClientRect().height - cache[firstIndex].height
                    listEl.scrollTop += dHeight;
                }
                [...listItems].forEach((listItem) => {
                    const rectBox = listItem.getBoundingClientRect();
                    const index = listItem.dataset.index;
                    const prevItem = cache[index-1];
                    const top = prevItem ? prevItem.bottom : 0;
                    Object.assign(cache[index], {
                        height: rectBox.height,
                        top,
                        bottom: top + rectBox.height,
                        isUpdate: true
                    });
                });
                // 切记一定要更新未渲染的listItem的top值
                for(let i = lastIndex+1; i < cache.length; i++) {
                    const prevItem = cache[i-1];
                    const top = prevItem ? prevItem.bottom : 0;
                    Object.assign(cache[i], {
                        top,
                        bottom: top + cache[i].height
                    });
                }
                oldFirstIndex = firstIndex;
            }
            
            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, rendered) {
                const startIndex = findItemIndex(listEl.scrollTop);
                const endIndex = startIndex + viewCount;
                // const visiblityEndIndex = findItemIndex(clientHeight + listEl.scrollTop);
                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]))
                }
                const startOffset = getStartOffset(startBufferIndex);
                const endOffset = getTotalSize() - getEndOffset(endBufferIndex);
                rendered(renderItems);
                // 渲染完成后,才更新缓存的高度信息
                upCellMeasure();
                listInner.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);
            }
        }
        
        // 模拟1万条数据
        const count = 10000;
        const items = Array.from({ length: count }).map((item, i) => ({ name: `item ${(i)}`, height: randomIncludes(40, 120) }) );
        const autoSizeVirtualList = initAutoSizeVirtualList({ listEl, listInner, clientHeight, items });

        document.addEventListener('DOMContentLoaded', () => {
            autoSizeVirtualList((item, rectBox) => {
                return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`
            }, (renderItems) => {
                listInner.innerHTML = renderItems.join('');
            });
        });

        listEl.addEventListener('scroll', throttle(() => {
            autoSizeVirtualList((item, rectBox) => {
                return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`
            }, (renderItems) => {
                listInner.innerHTML = renderItems.join('');
            });
        }));
    </script>
</body>
</html>

 


标签:原生,const,top,cache,列表,height,let,return,JS
From: https://www.cnblogs.com/littleboyck/p/17613887.html

相关文章

  • Spring Boot返回Json
    我们在前面的接口示例中是直接返回的字符串如下:但是我们有时候是需要返回json对象的。SpringBoot封装了JSON解析包Jackson的功能,只需要直接返回一个实体即可实现json的格式。如下:新建实体Sex.javapackagecom.biologic.entity;publicclassSex{privateStringsex;......
  • 「JSOI2008」最小生成树计数 题解报告
    简要题意现在给出了一个简单无向加权图。你希望知道这个图中有多少个不同的最小生成树。(如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的)。输出方案数对\(31011\)取模。SOLUTION这个题求最小生成树的方案所以我们从最小生成树入手(根据kruskal的思路)我们......
  • 三. JSON数据解析(parse)
    三.JSON数据解析(parse)专栏目录一.JSON二.JSON基础数据结构三.JSON数据解析(parse)0.引我们现在已经将JSON的基础数据结构进行了C语言抽象了,就像已经准备好了房子,就等着入住了,一般来说,JSON数据是以字符串形式由外部传入的,被解析的对象就是这个JSON格式的......
  • 人人贷(中js逆向学会模块改写(define/require/exports/module)
    链接https://renrendai.com/login?returnUrl=%2F这里逆向登入中的password加密 我们需要定位到password加密到位置,通过下面的搜索j_password,或者通过栈调用方式查找 可以看到加密的是这种模式define("common:node_modules/glpb-components-common/src/rsa/rsaCrypt",f......
  • web开发----jsp中通用模版的引用 include的用法
    web开发中常常会有一些代码需要多个页面使用,比如banner nav导航 还有footer等.ASP.NET开发中有母版页的说法,也就是写一些通用的模版页,然后其他页面可以引用。 jsp中 当然也有这样的用法 也就是include的用法 两种用法一种是说明标签<%@include file="xxx.jsp"%>,......
  • js记住用户名密码
    现在很多浏览器都会提供是否记住密码的功能。当我们写登录模块是 如果是使用form提交则能被浏览器识别到但是form提交会在链接上暴露出传的参数如果是用js做的登录 浏览器是检测不到的这样我们需要自己加上js记住密码的功能 主要用到了cookie.js以及login.jsp代码如下:co......
  • 云原生周刊:KubeCon China 2023 详细议程公布 | 2023.8.7
    开源项目推荐SpiderpoolSpiderpool是一个Kubernetes底层网络解决方案。它提供丰富的IPAM功能和CNI集成能力,为开源社区的CNI项目提供支持,允许多个CNI有效协作。它能让底层CNI在裸机、虚拟机和任何公共云等环境中完美运行。PreevyPreevy是一款功能强大的命令行界......
  • nodejs版本控制——nvm
    1、安装nvm首先要保证之前没有安装过nodejs,如果之前安装过,就先卸载:brewuninstallnodebrewinstallnvm 2、查看是否安装nvm-v 3、临时环境变量配置vi~/.bash_profile添加exportNVM_DIR="$([-z"${XDG_CONFIG_HOME-}"]&&printf%s"${HOME}/.nvm"......
  • 如何支持同一台电脑上使用不同版本的Node.js版本
    在我们实际项目开发过程中,经常不同项目使用的node.js版本会也有所不同,为了方便维护不同版本的项目。可以使用nvm来解决。1、下载nvm https://github.com/coreybutler/nvm-windows/releases2、执行nvm-setup.exe完成安装3、命令查找得到最新的nodejs版本:nvmlistavailable4、......
  • 云原生可观测框架 OpenTelemetry 基础知识(架构/分布式追踪/指标/日志/采样/收集器)
    什么是OpenTelemetry?OpenTelemetry是一个开源的可观测性框架,由云原生基金会(CNCF)托管。它是OpenCensus和OpenTracing项目的合并。旨在为所有类型的可观测信号(如跟踪、指标和日志)提供单一标准。https://opentelemetry.iohttps://www.cncf.iohttps://opencensus.io......