• 长列表网页相信大多数开发者都遇到过,在 DOM 元素过多的情况下,浏览器渲染会很慢,非常影响用户体验;

  • 因此我们会经常采用虚拟滚动、分页、上拉加载更多等不同的方式来进行优化,这些方式的思想都是一样的,都是只渲染可见区域,等用户需要时再加载更多的内容;

虚拟列表核心原理

  1. 整个虚拟列表划分为三个区域,分别是上缓冲区(0/2个元素),可视区(n个元素),下缓冲区(2个元素);

  2. 当滚动到一个元素离开可视区范围内时,就去掉上缓冲区顶上的一个元素,然后再下缓冲区增加一个元素,这就是虚拟列表的核心原理了;

虚拟列表的实现

元素固定高度

  1. 实现:

    • 首先先计算出由 1000 个元素撑起的盒子(称之为 container )的高度,撑开盒子,让用户能进行滚动操作;
    • 计算出可视区的起始索引、上缓冲区的起始索引以及下缓冲区的结束索引(就像上图滚动后,上缓冲区的起始索引为 2,可视区起始索引为 4,下缓冲区结束索引为 9);
    • 采用绝对定位,计算上缓冲区到下缓冲区之间的每一个元素在 contianer 中的 top 值,只有知道 top 值才能让元素出现在可视区内;
    • 将上缓冲区到下缓冲区的元素塞到 container 中;
  2. 实现代码:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    
        <style>
            #container {
                width: 200px;
                height: 200px;
                position: relative;
                overflow: auto;
                background: #eee;
            }
    
            .virtually-item:nth-child(odd) {
                position: absolute;
                width: 100%;
                text-align: center;
                background-color: bisque;
            }
    
            .virtually-item:nth-child(even) {
                position: absolute;
                width: 100%;
                text-align: center;
                background-color: aquamarine;
            }
        </style>
    </head>
    
    <body>
        <!-- 容器 -->
        <div id="container"></div>
    
        <script>
            const domContainer = document.getElementById('container');
    
            const FixedSizeList = ({ height, itemSize, itemCount }) => {
                // 记录滚动掉的高度
                const scrollOffset = domContainer.scrollTop;
    
                const getCurrentChildren = () => {
                    // 可视区起始索引
                    const startIndex = Math.floor(scrollOffset / itemSize);
                    // 上缓冲区起始索引
                    const finialStartIndex = Math.max(0, startIndex - 2);
                    // 可视区能展示的元素的最大个数
                    const numVisible = Math.ceil(height / itemSize);
                    // 下缓冲区结束索引
                    const endIndex = Math.min(itemCount - 1, startIndex + numVisible + 2);
    
                    const items = [];
                    // 根据上面计算的索引值,不断添加元素给 container
                    for (let i = finialStartIndex; i <= endIndex; i++) {
                        // 计算每个元素在 container 中的 top 值
                        const itemStyle = `height: ${itemSize}px; top: ${itemSize * i}px; line-height: ${itemSize}px;`;
                        items.push(`<div class="virtually-item" data-inx="${i}" style="${itemStyle}">${i}</div>`);
                    }
                    return items;
                }
    
                // 1000 个元素撑起盒子的实际高度
                return `<div style='height: ${itemSize * itemCount}px;'>${getCurrentChildren().join('')}</div>`;
            };
    
            // 当触发滚动就重新计算
            domContainer.addEventListener('scroll', function (event) {
                domContainer.innerHTML = FixedSizeList({ height: 200, itemSize: 50, itemCount: 1000 });
            });
    
            domContainer.innerHTML = FixedSizeList({ height: 200, itemSize: 50, itemCount: 1000 });
        </script>
    </body>
    
    </html>
    

元素不定高度

  1. 难点一:由于每个元素高度不一,无法直接计算出 container 的总高度;

    • 可以通过遍历所有的 Row 计算出总高度,但计算出精确总高度的必要性不大,同时也为了兼容第三种虚拟列表,不去计算精确的总高度;
    • 现在回到出发点,思考 container 的高度的作用是什么?其实就是为了足够大,让用户能进行滚动操作,那可以自己假设每一个元素的高度,在乘上个数,弄出一个假的但足够高的container 让用户去触发滚动事件,当然这种方案会带来一些小 bug(这个 bug 的影响不大,可以忽略);
  2. 难点二:每个元素高度不一,每个元素的 top 值不能通过 itemSize * index 直接计算出 top 值;

  3. 难点三:每个元素高度不一,不能直接通过 scrollOffset/itemSize 计算出已被滚动掉的元素的个数,很难获取到可视区的起始索引;

    • 其实难点二和难点三本质都一样,元素高度不一,导致不知道被滚动掉了多少元素,只要知道被滚动掉的元素的个数,top 值和索引都迎刃而解;
    • 可以采用这种解决方案,那就是每次只需要计算上缓冲区到下缓冲区之间的元素,并记录他们,并且记录下最底下的那个元素的索引,当用户进行向上滚动时,就可以直接从已经计算好的记录里取,如果向下滚动,根据上一次记录的最大的索引的那个元素不断累加新元素的高度,直到它大于已经滚动掉的高度,此时的索引值就是可视区的起始索引了,这个起始索引所对应的 top 就是累加的高度;
    • 图解:每一个元素的 top 值都能通过上一个元素的 top 值 + 上一个元素的 height 计算出来;
    • 举个例子,假设我们需要知道 item14 的 top 值;
    • 先在记录里找有没有 item13 的数据,如果有则就拿 item13.top + item13.heighht 得到 item14 的 top;
    • 如果记录中没有(由上图得知只记录了item1-item10 的数据),则拿到记录中最后一个元素的数据(item10)进行累加,先计算并记录 item11 的,再计算并记录 item12 的,再计算并记录item13 的,最后就是 item14 的了;

元素动态高度

最后这一种虚拟列表其实就是基于第二种来实现的,只不过增加监听元素高度变化事件,在某个元素发生变化的时候重新计算各种数据;

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 虚拟列表核心原理
  2. 2. 虚拟列表的实现
    1. 2.1. 元素固定高度
    2. 2.2. 元素不定高度
    3. 2.3. 元素动态高度