当前位置: 首页 > article >正文

JS实现高度不等的列表虚拟滚动加载

       当我们拿到后台返回的1万条数据展示时,如果完全渲染不仅渲染的时间很长,也会导致浏览器性能变差,使用过程中还可能会导致卡顿,使用体验变差。
就需要我们想办法优化这种情况,这个时候使用虚拟滚动加载就能很好的避免这种情况。

虚拟滚动的实现原理:只加载用户当前可见区域内的内容,即滚动列表时,动态改变可视区域内的渲染内容
在这里插入图片描述
根据这个原理,我们需要知道一屏加载多少条数据,设置一个默认的子元素高度,根据展示区域计算出一个可见条数,再设置一些缓存区数量,避免滚动就需要重新加载

// 设置的默认子元素高度
var minSize = 25
// 子元素默认高度 情况下可见数量条数
var viewCount = Math.ceil(dom.clientHeight / minSize)
// 缓存区数量(虚拟滚动当前屏展示量以外 多展示的条目)
var bufferSize = 6
// 默认条数+缓存区数量后的总条数
var bufferSizeViewCount = viewCount + bufferSize * 2 

因为是不等高列表,当我们跳转指定行或者做其他更多操作时,需要知道指定行的具体信息,所以缓存所有子元素的布局信息。先设置每行默认的布局信息,后续页面滚动加载时再实时更新

// 缓存位置信息
var topNum = 0
var bottomNum = 0
var height = minSize
for (let index = 0; index < tableData.length; index++) {
    const item = tableData[index];
    topNum = index == 0 ? 0 : (topNum + height)
    bottomNum = topNum + height
    const obj2 = {
        index,
        height,
        top: topNum,
        bottom: bottomNum,
        isUpdate: false
    }
    rowPosList.push(obj2);
}

根据需要加载的开始位置和结束位置,加载虚拟滚动页面的内容,默认加载从第一行开始的数据

// 当前开始下标(滚动top)
let startIndex = 0;
// 当前结束下标(根据盒子高度时可见数量条数得到结束下标)
let endIndex = Math.min(tableData.length - 1, startIndex + viewCount);
// 算上缓存区数量后,真实需要加载的起始行和结束行下标
let startBufferIndex = Math.max(0, startIndex - bufferSize);
let endBufferIndex = Math.min(tableData.length - 1, endIndex + bufferSize);
// 生成dom节点
let documentFragment = document.createDocumentFragment()
for (let i = startBufferIndex; i <= endBufferIndex; i++) {
    const item = tableData[i]
    // 当前行展示内容
    let htmlText = "<li class='liText' rowindex='"+item.index+"'>序号:" + item.index +",内容:"+ item.text +"</li>"
    // 将 HTML 字符串转换为 DOM 元素,并添加到文档片段中
    const tempElement = document.createElement('template');
    tempElement.innerHTML = htmlText;
    const elList = [].slice.call(tempElement.content.children)
    for (const el of elList) {
        documentFragment.appendChild(el);
    }
}
listInnerDom.innerHTML = ""
listInnerDom.appendChild(documentFragment)

加载成功后就需要设置滚动条,为了后面滚动加载其他的内容,我们使用padding撑开盒子的高度,而padding的值就根据缓存的rowPosList每一元素的布局信息来设置

// 设置滚动条高度(利用上下边距撑开)
const startOffset = rowPosList[startBufferIndex].top;
const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;
listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);

滚动加载就意味着需要根据滚动的高度获取需要加载内容,我们使用滚动top值从缓存信息rowPosList的top、bottom值来进行判断获取当前滚动距离对应的行下标

// 二分查询优化
function findItemIndex(scrollTop) {
    let low = 0;
    let high = rowPosList.length - 1;
    while (low <= high) {
        const mid = Math.floor((low + high) / 2);
        const { top, bottom } = rowPosList[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;
}

虚拟滚动实际上就是移除旧的dom,插入新dom的操作,如果操作过于频繁就会影响性能,所以我们需要进行限制。因为我们加了缓存区的概念,所以理所当然的在缓存区范围内滚动就不需要再重新加载内容

// 在缓存区范围以外才进行渲染更新
if(startIndex < startBufferIndex || endIndex > endBufferIndex){
}

而为了后续的一些针对列表的操作,例如直接查看第666行的数据,就需要对这行的数据有清楚的定位信息,且确保跳转后再次上下滚动加载不会导致出现问题(白屏情况),所以需要实时更新之前的布局信息以供这种操作

// 更新每个item的位置信息 
function upCellMeasure() {
   const rowList = listInnerDom.querySelectorAll(".liText");
   if (rowList.length === 0) { return }
   const firstIndex = startBufferIndex;
   const lastIndex = endBufferIndex;

   // 当前渲染的数据高度是否有改动,有则需要修改当前渲染数据后面所有存储的坐标信息
   let hasChange = false
   let dHeight = 0
   for (let index = 0; index < rowList.length; index++) {
       const rowItem = rowList[index];
       const rowIndex = rowItem.getAttribute("rowindex");
       let rowPosItem = rowPosList[rowIndex]
       const prevRowPosItem = rowPosList[rowIndex - 1];
       // 设置当前行的top|bottom|height等属性可能导致列高度的改变,已经设置过的不再重新设置
       if (rowPosItem && (!rowPosItem.isUpdate || (prevRowPosItem && prevRowPosItem.top + prevRowPosItem.height != rowPosItem.top))) {
           const rectBox = rowItem.getBoundingClientRect();
           const top = prevRowPosItem ? prevRowPosItem.bottom : 0;
           let height = rectBox.height
           dHeight = dHeight + (height - rowPosItem.height)
           Object.assign(rowPosList[rowIndex], {
               height,
               top,
               bottom: top + height,
               isUpdate: true
           });
           hasChange = true
       }
   }
   // // 鼠标滑轮滚动 解决向上滚动时,前面行的坐标信息未更新,会导致后面设置padding的值导致滚动偏移
   // // 在直接跳转到指定行后,向上滚动,在下一篇幅重新渲染后pdfMain的scrollTop会向下偏移,所以使用之前scroll事件记录的domScrollTop进行累加计算
   // if(mouseInTable && firstIndex < oldFirstIndex) {
   //   maskDom.scrollTop= domScrollTop + dHeight;
   // }
   // 更新未渲染的listItem的top值
   if (hasChange) {
       for (let i = lastIndex + 1; i < rowPosList.length; i++) {
           // if(!rowPosList[i].isUpdate){
           const prevRowPosItem = rowPosList[i - 1];
           const top = prevRowPosItem ? prevRowPosItem.bottom : 0;
           Object.assign(rowPosList[i], {
               top,
               bottom: top + rowPosList[i].height
           });
           // }
       }
   }
   // console.log(rowPosList)
   // 设置滚动条高度(利用上下边距撑开)
   const startOffset = rowPosList[startBufferIndex].top;
   const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;
   listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);
   oldFirstIndex = firstIndex;
}

完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟滚动加载不等高列表</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        button {
            width: 100px;
            margin: 20px 0 0 50px;
        }
        #masks {
            position: relative;
            width: 500px;
            height: 750px;
            margin: 20px 50px;
            overflow: auto;
            border: 1px solid #666;
        }
        .ulBox{
            height: 100%;
        }
        .listInner li{
            list-style: none;
            border-bottom: 1px solid #ccc;
            padding: 5px 10px;
        }
    </style>
</head>

<body>
    <button onclick="jumpRow(666)">跳转第666</button>
    <div class="pdf-main" id="masks" onscroll="myScroll()">
        <div class="ulBox">
            <ul class="listInner">
                <!-- 加载数据 -->
            </ul>
        </div>
    </div>
    <script>

        // 随机生成1万条不同内容的数组数据
        var tableData = []
        var datalist = "神鼎飞丹砂发生过大范甘迪发生过发给的孤独果然废物感染树大根深单个重新发但是山东分公司电话该发给非固定阿斯顿发顺丰阿萨法打算神鼎飞丹砂发生过电视上地方郭发的郭旦法大范甘迪的的高度房"
        var length = datalist.length
        for (let index = 0; index < 10000; index++) {
            var randomNumber0 = Math.floor(Math.random() * length);
            var randomNumber1 = Math.floor(Math.random() * length);
            var num1 = Math.min(randomNumber0, randomNumber1)
            var num2 = Math.max(randomNumber0, randomNumber1)
            tableData.push({ index, text: datalist.slice(num1, num2) })
        }

        // 缓存的每一行的信息(height、top、bottom等等)
        var rowPosList = []
        // 设置的默认子单元高度
        var minSize = 25
        // 子单元默认高度 情况下可见数量条数
        var viewCount = 0
        // 缓存区数量(虚拟滚动当前屏展示量以外 多展示的条目)
        var bufferSize = 6
        // 默认条数+缓存区数量后的总条数
        var bufferSizeViewCount = 0 

        var maskDom = document.getElementById("masks")
        // 缓存位置信息
        var topNum = 0
        var bottomNum = 0
        var height = minSize
        for (let index = 0; index < tableData.length; index++) {
            const item = tableData[index];
            topNum = index == 0 ? 0 : (topNum + height)
            bottomNum = topNum + height
            const obj2 = {
                index,
                height,
                top: topNum,
                bottom: bottomNum,
                isUpdate: false
            }
            rowPosList.push(obj2);
        }
        viewCount = Math.ceil(maskDom.clientHeight / minSize);
        bufferSizeViewCount = viewCount + bufferSize * 2
        console.log(tableData,rowPosList)

        var showTableDataList = []      // 当前虚拟滚动展示的数据
        var domScrollTop = 0     // 滚动距离
        var requestId = ""  // 限制当前屏是否完全加载
        var startBufferIndex = -1   // 当前加载数据中第一个所在下标
        var endBufferIndex = -1  // 当前加载数据中最后一个所在下标
        var oldFirstIndex = 0  // 当前加载数据中第一个所在下标(旧值)
        var listInnerDom = document.querySelector(".listInner")
        // 更新加载的数据(初始页面加载、滚动加载、点击选中列)
        autoSizeVirtualList(0)

        // 更新加载的数据(初始页面加载、滚动加载、跳转指定行)
        function autoSizeVirtualList(scrollTop, jumpRow_no) {
            // 根据滚动的高度值 || 或者需要跳转至指定行的行下标,判断当前加载下标
            let startIndex = jumpRow_no != undefined ? jumpRow_no : findItemIndex(scrollTop);
            const endIndex = Math.min(tableData.length - 1, startIndex + viewCount);
            // 页面是否重新加载
            let dataChange = false

            // 判断当前 startIndex 是否在缓冲区内。在缓冲区内,不需要重新渲染
            if (endIndex > 0 && (startIndex < startBufferIndex || endIndex > endBufferIndex)) {
                startBufferIndex = Math.max(0, startIndex - bufferSize);
                endBufferIndex = Math.min(tableData.length - 1, endIndex + bufferSize);

                showTableDataList = []
                let documentFragment = document.createDocumentFragment()
                for (let i = startBufferIndex; i <= endBufferIndex; i++) {
                    const item = tableData[i]
                    const rectBox = rowPosList[i]
                    showTableDataList.push({ item, rectBox })

                    // 当前行展示内容
                    let htmlText = "<li class='liText' rowindex='"+item.index+"'>序号:" + item.index +",内容:"+ item.text +"</li>"
                    // 将 HTML 字符串转换为 DOM 元素,并添加到文档片段中
                    const tempElement = document.createElement('template');
                    tempElement.innerHTML = htmlText;
                    const elList = [].slice.call(tempElement.content.children)
                    for (const el of elList) {
                        documentFragment.appendChild(el);
                    }
                }
                showTableDataList = showTableDataList
                listInnerDom.innerHTML = ""
                listInnerDom.appendChild(documentFragment)
                dataChange = true
            }
            // 重新加载后需要更新缓存的定位信息
            if (dataChange) {
                upCellMeasure();
            }
            // 跳转到指定行查看
            if (jumpRow_no != undefined) {
                maskDom.scrollTop = rowPosList[jumpRow_no].top
            }
            domScrollTop = maskDom.scrollTop
            requestId = null;
        }

        // 二分查询优化
        function findItemIndex(scrollTop) {
            let low = 0;
            let high = rowPosList.length - 1;
            while (low <= high) {
                const mid = Math.floor((low + high) / 2);
                const { top, bottom } = rowPosList[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的位置信息 
        function upCellMeasure() {
            const rowList = listInnerDom.querySelectorAll(".liText");
            if (rowList.length === 0) { return }
            const firstIndex = startBufferIndex;
            const lastIndex = endBufferIndex;

            // 当前渲染的数据高度是否有改动,有则需要修改当前渲染数据后面所有存储的坐标信息
            let hasChange = false
            let dHeight = 0
            for (let index = 0; index < rowList.length; index++) {
                const rowItem = rowList[index];
                const rowIndex = rowItem.getAttribute("rowindex");
                let rowPosItem = rowPosList[rowIndex]
                const prevRowPosItem = rowPosList[rowIndex - 1];
                // 设置当前行的top|bottom|height等属性可能导致列高度的改变,已经设置过的不再重新设置
                if (rowPosItem && (!rowPosItem.isUpdate || (prevRowPosItem && prevRowPosItem.top + prevRowPosItem.height != rowPosItem.top))) {
                    const rectBox = rowItem.getBoundingClientRect();
                    const top = prevRowPosItem ? prevRowPosItem.bottom : 0;
                    let height = rectBox.height
                    dHeight = dHeight + (height - rowPosItem.height)
                    Object.assign(rowPosList[rowIndex], {
                        height,
                        top,
                        bottom: top + height,
                        isUpdate: true
                    });
                    hasChange = true
                }
            }
            // // 鼠标滑轮滚动 解决向上滚动时,前面行的坐标信息未更新,会导致后面设置padding的值导致滚动偏移
            // // 在直接跳转到指定行后,向上滚动,在下一篇幅重新渲染后pdfMain的scrollTop会向下偏移,所以使用之前scroll事件记录的domScrollTop进行累加计算
            // if(mouseInTable && firstIndex < oldFirstIndex) {
            //   maskDom.scrollTop= domScrollTop + dHeight;
            // }
            // 更新未渲染的listItem的top值
            if (hasChange) {
                for (let i = lastIndex + 1; i < rowPosList.length; i++) {
                    // if(!rowPosList[i].isUpdate){
                    const prevRowPosItem = rowPosList[i - 1];
                    const top = prevRowPosItem ? prevRowPosItem.bottom : 0;
                    Object.assign(rowPosList[i], {
                        top,
                        bottom: top + rowPosList[i].height
                    });
                    // }
                }
            }
            // console.log(rowPosList)
            // 设置滚动条高度(利用上下边距撑开)
            const startOffset = rowPosList[startBufferIndex].top;
            const endOffset = rowPosList[rowPosList.length - 1].bottom - rowPosList[endBufferIndex].bottom;
            listInnerDom.style.setProperty('padding', `${startOffset}px 0 ${endOffset}px 0`);
            oldFirstIndex = firstIndex;
        }
        
        // 滚动加载数据
        function myScroll() {
            if (requestId) return;
            requestId = requestAnimationFrame(() => {
                let scrollTop = maskDom.scrollTop
                let lastItem = tableData[tableData.length - 1]
                let lastShowItem = showTableDataList[showTableDataList.length - 1].item
                // 判断向下滚动到最后一篇幅时,就不再更新数据(即滚到底了就不更新)
                if (lastItem.index == lastShowItem.index && scrollTop >= domScrollTop) {
                    requestId = null;
                    return
                }
                domScrollTop = scrollTop
                // 更新加载的数据(初始页面加载、滚动加载、点击选中列)
                autoSizeVirtualList(scrollTop)
            });
        }

        // 跳转到指定行
        function jumpRow(jumpRow_no){
            autoSizeVirtualList(undefined,jumpRow_no)
        }
    </script>
</body>

</html>

效果如下:

在这里插入图片描述


http://www.kler.cn/a/289184.html

相关文章:

  • 今日 AI 简报 | 开源 RAG 文本分块库、AI代理自动化软件开发框架、多模态统一生成框架、在线图像背景移除等
  • 车-路-站-网”信息耦合的汽车有序充电
  • Python数据类型(一):bool布尔类型
  • C++面试基础知识:排序算法 C++实现
  • C++模板特化实战:在使用开源库boost::geometry::index::rtree时,用特化来让其支持自己的数据类型
  • 21. Drag-Drop拖放操作(二) - 文件、表格和树的拖放实现
  • mysql迁移到达梦数据库报错:列[xx]长度超出定义
  • subclass-balancing的related work+conclusion
  • 智能合约漏洞(五)
  • AI大模型编写多线程并发框架(六十五):发布和应用
  • vue 动态替换父组件
  • salesforce flow 更新记录,某一个更新失败会导致所有失败吗
  • SpringMVC基于注解的使用
  • 系统编程-数据库
  • [开源]低代码表单FormCreate的control表单联动功能的详解
  • vue3中使用supermap icilent3d for cesium
  • 【深度学习 CV方向】图像算法工程师 职业发展路线,以及学习路线
  • SSMA for MySQL 将MySQL数据导入SqlServer
  • Xilinx FFT IP使用
  • 【流式输出】LangChain流式输出的概念
  • Python教程:使用 Python 和 PyHive 连接 Hive 数据库
  • windows核心编程,纤程使用---->UNIX服务器应用程序移植到Windows中
  • 在更新python虚拟环境pip版本时,由于更新失败导致pip丢失的修复
  • GD - EmbeddedBuilder - 给已有工程换MCU
  • 【IPV6从入门到起飞】2-1 获取你的IPV6(手机、CPE等)
  • LVS Keepalived nginx haproxy 区别