虚拟滚动-基本的虚拟滚动demo

304 阅读2分钟

虚拟滚动

背景-why

  1. 假设页面上有一个有大量数据的列表需要展示,每一行都是一个单独向项目,那么页面上就会有大量的dom元素
  2. 这些dom元素并不会完全的展示在页面上,只有视窗内的会被展示出来
  3. 存在大量的隐藏的dom元素,会造成页面的卡顿

什么是虚拟滚动-what

  1. 虚拟滚动是利用列表中有大量元素隐藏的特点,只将页面中能够展示的元素挂在dom中
  2. 页面发生滚动的时候,通过当前滚动的高度,来计算出当前应该在页面中展示的元素,然后将其挂载在dom中
  3. 概念 3. 内容项:单个展示项目 2. 窗口:当前页面中用来展示内容项的区域
    1. 容器:一个高度为所有内容项目总高度的元素,用来撑开窗口高度,容纳内容项

如何实现虚拟滚动-how

  1. 设置展示窗口和滚动元素及定义
<div class="viewport">                      窗口
  <div class="content"></div>               容器 
</div>

<style>
  .viewport {
      height: 300px;
      overflow: auto;
  }

  .content {
      height: 1000px;
      /* 实际内容高度 */
      padding: 20px;
  }
</style>
<script>
    const viewport = document.querySelector('.viewport');
    const content = document.querySelector('.content');

    const ITEM_HEIGHT = 50; // 每个项的高度
    const ITEM_COUNT = 10000; // 项的总数
    
    // 内容项构造
    function createItem(index) {
        const item = document.createElement('div');
        item.classList.add('item');
        item.textContent = `Item ${index}`;
        item.style.height = `${ITEM_HEIGHT}px`;
        return item;
    }
</script>
  1. 渲染函数
function render(viewport, content) {
    // 传入窗口 容器
    const scrollTop = viewport.scrollTop;
    const viewportHeight = viewport.clientHeight;

    // 计算当前应该展示的元素的索引
    const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
    const endIndex = Math.min(
        Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT),
        ITEM_COUNT
    );

    // 生成当前窗口中应当展示的元素
    const items = [];
    for (let i = startIndex; i < endIndex; i++) {
        const item = createItem(i);
        items.push(item);
    }

    // 重置 容器中html的内容
    content.innerHTML = '';

    // 每一次滚动事件都会重新设置一次padding,使得当前容器中的元素能够整好显示在窗口中
    content.style.paddingTop = `${startIndex * ITEM_HEIGHT}px`;

    // 将元素加入到容器中
    content.append(...items);
}

  1. 监听窗口滚动事件
viewport.addEventListener('scroll', () => {
    // 滚动事件的时候触发渲染
    render(viewport, content)
});
  1. 整体demo

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>虚拟滚动实现</title>
    <style>
        .viewport {
            height: 300px;
            overflow: auto;
        }

        .content {
            height: 1000px;
            /* 实际内容高度 */
            padding: 20px;
        }

        .item {
            background-color: #eee;
            border: 1px solid #ccc;
            line-height: 50px;
            padding: 10px;
        }
    </style>
</head>

<body>
    <div class="viewport">
        <div class="content"></div>
    </div>

    <script>
        const viewport = document.querySelector('.viewport');
        const content = document.querySelector('.content');

        const ITEM_HEIGHT = 50; // 每个项的高度
        const ITEM_COUNT = 10000; // 项的总数

        const Total_Height = ITEM_HEIGHT * ITEM_COUNT

        content.style.height = Total_Height

        // 首次渲染
        render(viewport, content)

        viewport.addEventListener('scroll', () => {
            render(viewport, content)
        });

        function createItem(index) {
            const item = document.createElement('div');
            item.classList.add('item');
            item.textContent = `Item ${index}`;
            item.style.height = `${ITEM_HEIGHT}px`;
            return item;
        }

        function render(viewport, content) {
            const scrollTop = viewport.scrollTop;
            const viewportHeight = viewport.clientHeight;
            const contentHeight = content.clientHeight;

            // 计算当前应该展示的元素的索引
            const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
            const endIndex = Math.min(
                Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT),
                ITEM_COUNT
            );

            // 生成当前窗口中应当展示的元素
            const items = [];
            for (let i = startIndex; i < endIndex; i++) {
                const item = createItem(i);
                items.push(item);
            }

            // 重置 容器中html的内容
            content.innerHTML = '';

            // 每一次滚动事件都会重新设置一次padding,使得当前容器中的元素能够整好显示在窗口中
            content.style.paddingTop = `${startIndex * ITEM_HEIGHT}px`;

            // 将元素加入到容器中
            content.append(...items);
        }

    </script>
</body>

</html>

更多优化思路

  1. 优化
    1. transform代替设置padding,可以有效利用gpu加速
    2. 现在滚动过程中每次都是在重新的生成所有的元素,但是页面上元素个数是固定的。滚动的过程中,如果元素一直在页面上,可以将其移动,对于消失的元素和新出现的元素将其内容替换,即将消失的元素的内容替换为新出现元素的内容,并将其移动到新出现元素的位置。这样可以避免新生成元素
    3. 上下额外渲染部分区域,可以在小范围滚动的时候优化体验
    4. 滚动节流,降低渲染频率
  2. 性能指标
    1. 浏览器渲染帧率
    2. 内存占用
    3. 重绘、重排