原生JS实现虚拟列表(不使用Vue,React等前端框架)

好家伙,

1. 什么是虚拟列表

虚拟列表(Virtual List)是一种优化长列表渲染性能的技术。当我们需要展示成千上万条数据时,如果一次性将所有数据渲染到DOM中,会导致页面卡顿甚至崩溃。虚拟列表的核心思想是:只渲染可视区域内的数据,而不是渲染所有数据

2. 使用场景

虚拟列表适用于以下场景:

  • 大数据量展示:如聊天记录、新闻列表、商品列表等需要展示大量数据的场景
  • 无限滚动:需要支持用户持续滚动加载更多内容的场景
  • 性能敏感:在低性能设备上运行的应用,需要尽可能减少DOM操作
  • 实时数据更新:频繁更新的数据列表,如股票行情、实时监控数据等

(我觉得实际场景中,分页会用到更多,用户要看的数据,永远只是一小部分,就那么几条,找不到就用搜索

但总要学学)

3. 虚拟列表原理

一句话:

要看了,再渲染

对,就这么简单,下面,进行分步

  • 计算可视区域:确定用户当前可以看到的视口范围
  • 计算可见项:根据视口位置、每项高度,计算出当前应该显示哪些数据项
  • 渲染可见项:只渲染计算出的可见项到DOM中
  • 位置偏移:通过CSS定位,确保可见项在正确的位置显示
  • 监听滚动:当用户滚动时,重新计算可见项并更新DOM

这里几个难点:
我怎么知道哪些数据进入了可视区域?
答:监听滚动距离,滚到哪,就从哪里开始

4. 实现虚拟列表

Demo.html代码如下:

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原生JavaScript虚拟列表实现</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        .list-container {
            position: relative;
            height: 400px;
            overflow: auto;
            border: 1px solid #ccc;
            margin: 20px auto;
            width: 80%;
        }
        
        .list-phantom {
            position: absolute;
            left: 0;
            top: 0;
            right: 0;
            z-index: -1;
        }
        
        .list-content {
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            overflow: hidden;
        }
        
        .list-item {
            padding: 10px;
            border-bottom: 1px solid #eee;
            color: #666;
        }
        
        .list-item:hover {
            background-color: #f5f5f5;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center; margin: 20px 0;">原生JavaScript虚拟列表</h1>
    <div id="virtualList" class="list-container">
        <div class="list-phantom"></div>
        <div class="list-content"></div>
    </div>

    <script>
        class VirtualList {
            constructor(options) {
                this.container = options.container;
                this.data = options.data || [];
                this.itemHeight = options.itemHeight || 50;
                this.bufferSize = options.bufferSize || 5;
                
                this.phantom = this.container.querySelector('.list-phantom');
                this.content = this.container.querySelector('.list-content');
                
                this.startIndex = 0;
                this.endIndex = 0;
                this.scrollTop = 0;
                
                this.init();
            }
            
            init() {
                // 设置占位容器的高度
                this.phantom.style.height = this.data.length * this.itemHeight + 'px';
                
                // 监听滚动事件
                this.container.addEventListener('scroll', this.handleScroll.bind(this));
                
                // 初始渲染
                this.updateVisibleItems();
            }
            
            handleScroll() {
                // 获取当前滚动位置
                this.scrollTop = this.container.scrollTop;
                
                // 更新可见项
                this.updateVisibleItems();
            }
            
            updateVisibleItems() {
                // 计算开始和结束索引
                this.startIndex = Math.floor(this.scrollTop / this.itemHeight);
                this.endIndex = this.startIndex + Math.ceil(this.container.clientHeight / this.itemHeight);
                
                // 添加缓冲区
                this.startIndex = Math.max(0, this.startIndex - this.bufferSize);
                this.endIndex = Math.min(this.data.length, this.endIndex + this.bufferSize);
                
                // 计算偏移量
                const offsetY = this.startIndex * this.itemHeight;
                
                // 设置内容容器的偏移
                this.content.style.transform = `translateY(${offsetY}px)`;
                
                // 渲染可见项
                this.renderItems();
            }
            
            renderItems() {
                // 清空内容容器
                this.content.innerHTML = '';
                
                // 渲染可见项
                for (let i = this.startIndex; i < this.endIndex; i++) {
                    const item = document.createElement('div');
                    item.className = 'list-item';
                    item.innerHTML = this.renderItemContent(this.data[i], i);
                    item.style.height = this.itemHeight + 'px';
                    this.content.appendChild(item);
                }
            }
            
            renderItemContent(item, index) {
                return `<div>索引: ${index}, 内容: ${item}</div>`;
            }
        }
        
        // 生成测试数据
        const data = Array.from({ length: 10000 }, (_, i) => `列表项 ${i + 1}`);
        
        // 初始化虚拟列表
        const virtualList = new VirtualList({
            container: document.getElementById('virtualList'),
            data: data,
            itemHeight: 50,
            bufferSize: 10
        });
    </script>
</body>
</html>

5.最后总结

为什么滚动到指定位置后会将对应区域数据渲染?

1.监听滚动事件

2.滚动触发数据更新方法

3.根据滚动距离计算当前数据索引

4.根据可视区域计算要渲染数据项

5.渲染数据

6.定位内容