原生js实现虚拟列表-有高度

637 阅读3分钟

前言:长列表滚动卡顿问题属于比较常见的性能问题,在写这篇文章之前已有很多前辈写过解决方案,这里我只做记录当做自己笔记,有问题可以随时提出建议或意见。 虚拟列表主要解决长列表滚动卡顿问题,通过虚拟列表可以轻松实现数十万条数据的流畅滚动!

实现效果:

虚拟列表.gif

从上图可以清楚的看到当我们滚动内容的时候,dom结构始终只有几个标签存在,不会一直增加!

实现思路

整体实现思路:

  1. 根据列表项高度和数量计算并设置虚拟滚动条高度。
  2. 根据列表项的高度和内容可视区域的高度计算一屏展示的数量
  3. 根据列表项的高度和内容滚动的距离计算滚动到屏幕外的列表项数量,然后再根据一屏展示的数量计算渲染内容的在数组的开始和结束位置。
  4. 根据开始和结束位置渲染对应在数组中的内容

以下是具体的实现步骤:

1、设置列表项固定高度 itemHeight
2、计算并设置虚拟滚动条高度: 列表项数量*itemHeight
3、计算一屏展示的数量:viewCount = 容器高度/列表项高度
4、设置渲染内容的开始和结束位置
    startIndex=0 渲染内容对应在数据中索引初始值
    endIndex= viewCount 渲染内容对应在数组中索引结束值
5、封装函数实现渲染数据 render
    渲染的数据从starIndex开始到endIndex结束
6、监听滚动事件
    6-1、计算滚动屏幕外的内容有多少个列表项,即重新计算滚动后渲染数据索引的开始索引值
        startIndex = this.scrollTop/itemHeight
    6-2、计算滚动后的结束索引值
        endIndex = viewCount + startIndex
    6-3、重新渲染startIndex到endIndex的数据
    6-4、重新渲染后,需要把列表容器的位置重新位移到顶部
        偏移量的计算 = this.scrollTop - (this.scrollTop%itemHeight)
    6-5、通过transform属性设置列表容器位置

布局

html结构

    <!-- 滚动容器 -->
    <div class="virtual-box">
        <!-- 虚拟滚动条 -->
        <div class="virtual-scrollbar"></div>
        <!-- 滚动列表容器 -->
        <div class="virtual-list">
            <div class="virtual-item"></div>
        </div>
    </div>

样式

        *{
            margin: 0;
            padding: 0;
        }
        .virtual-box {
            width: 200px;
            height: 300px;
            margin: 20px auto;
            overflow: auto;
            border: 1px solid #ccc;
            position: relative;
        }
        .virtual-scrollbar {
            position: absolute;
            width: 100%;
            height: 100%;
        }
        /* 滚动列表容器高度“一定要添加”, 否则鼠标拖动滚动条到底部时列表会发生闪动问题 */
         .virtual-list {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
         }
        .virtual-item {
            height: 50px;
            line-height: 50px;
            padding: 5px 10px;
        }

功能实现

准备初始数据

let arr = []
for(let i=1;i<=1000;i++){
    arr.push("item-"+i)
}

1、获取需要用到的容器

// 获取滚动容器
const container = document.querySelector(".virtual-box")
// 获取虚拟滚动条
const scrollBar = document.querySelector(".virtual-scrollbar")
// 获取滚动(渲染)列表容器
const renderBox = document.querySelector(".virtual-list")

2、初始化数据

// 设置列表项高度
const itemHeight = 50;
// 设置滚动条高度
scrollBar.style.height = arr.length*itemHeight+"px"
// 一屏展示的数量
let viewCount = Math.ceil(container.offsetHeight/itemHeight)
// 设置展示数据在数组中的开始和结束位置
let startIndex = 0;
let endIndex = viewCount;

3、实现渲染数据

function render(startIndex, endIndex){
    // renderBox.innerHTML = ""
    // 判断数据已经展示完毕
    if(endIndex>arr.length){
        endIndex = arr.length
    }
    let divStr = ""
    for(let i = startIndex; i<endIndex;i++){
        divStr+=`<div class="virtual-item">${arr[i]}</div>`
    }
    // 更新列表内容
    renderBox.innerHTML = divStr
}
// 直接调用渲染函数,实现页面数据初始化
render(startIndex, endIndex)

4、监听滚动事件并更新开始和结束索引值

container.addEventListener("scroll", function(){
    // 计算当前移动了多少个列表项 = 滚动的高度/列表项的高度
    startIndex = Math.floor(this.scrollTop/itemHeight)
    endIndex = viewCount + startIndex
    render(startIndex, endIndex)
    // 滚动后 list容器位置会发生偏移,需要把容器移动到原来的位置
    let startOffset = this.scrollTop - (this.scrollTop % itemHeight)
    renderBox.style.transform = `translateY(${startOffset}px)`

})

5、完整代码

function virtualScroll(arr){
    // 获取需要用到的容器
    const container = document.querySelector(".virtual-box")
    const scrollBar = document.querySelector(".virtual-scrollbar")
    const renderBox = document.querySelector(".virtual-list")


    // 列表项高度
    const itemHeight = 50;
    // 设置滚动条高度
    scrollBar.style.height = arr.length*itemHeight+"px"
    // 一屏展示的数量
    const viewCount = Math.ceil(container.offsetHeight/itemHeight)
    // 设置展示数据在数组中的开始和结束位置
    let startIndex = 0;
    let endIndex = viewCount;


    // 渲染数据
    function render(startIndex, endIndex){
        // renderBox.innerHTML = ""
        // 判断数据已经展示完毕
        if(endIndex>arr.length){
            endIndex = arr.length
        }
        let divStr = ""
        for(let i = startIndex; i<endIndex;i++){
            divStr+=`<div class="virtual-item">${arr[i]}</div>`
        }

        renderBox.innerHTML = divStr
    }
    render(startIndex, endIndex)

    // 监听滚动
    container.addEventListener("scroll", function(){
        // 计算当前移动了多少个列表项 = 滚动的高度/列表项的高度
        startIndex = Math.floor(this.scrollTop/itemHeight)
        endIndex = viewCount + startIndex
        render(startIndex, endIndex)
        // 滚动后 list容器位置会发生偏移,需要把容器移动到原来的位置
        let startOffset = this.scrollTop - (this.scrollTop % itemHeight)
        renderBox.style.transform = `translateY(${startOffset}px)`

    })
}