用原生JS实现虚拟列表(IT枫斗者)

news2024/12/30 2:50:13

用原生JS实现虚拟列表

介绍

  • 最近在开发需求的时候,有用到 Antd 的虚拟列表组件 rc-virtual-list ,粗略地看了一下源码,于是萌生了自己写一个虚拟列表的想法。
  • 当一个列表需要渲染大量数据的时候是非常耗时的,而且在列表滚动的过程中会出现卡顿的现象。即使用上懒加载解决了列表初始化时渲染过慢的问题,但是每次拉取下一页数据的时候都会造成列表的重新渲染。随着拉取的数据越来越多,列表渲染时间长、卡顿的问题依然存在。
  • 这个时候虚拟列表就派上用场了。虚拟列表的实现原理简单来说,就是列表并不会把所有的数据都渲染出来,而是通过监听滚动事件然后实时计算当前是哪几条数据显示在页面上,然后只渲染用户可以看见的这几条数据。
  • 在网上找了张图可以说明的更生动些:
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vbz6hBM5-1684195418798)(C:\Users\quyanliang\AppData\Roaming\Typora\typora-user-images\1684195219996.png)]

定高虚拟列表

  • 实现虚拟列表需要两层的div:

  • <style>
      .list-container {
        overflow: auto;
        border: 1px solid black;
        height: 500px;
      }
    </style>
    
    <body>
      <!-- 外部容器用来固定列表容器的高度,同时生成滚动条 -->
      <div class="list-container">
        <!-- 内部容器用来装元素,高度是所有元素高度的和 -->
        <div class="list-container-inner"></div>
      </div>Ï
    </body>
    
  • 外层的div用来固定高度,也就是图上展示的B区域。内层的div用来装列表元素,也就是图上展示的A区域。给外层的div设置 overflow: auto ,这样当列表元素数量超过B区域的高度时就会出现滚动条。

  • 接下来,我们需要监听外层div的滚动事件,实时计算出显示在可视区域上的第一个元素的坐标 startIndex 和 最后一个元素的坐标 endIndex。假设列表每个元素的高度都是60px,那么 startIndex 就等于外层 div 顶部卷起来的长度除以列表元素的高度,而 endIndex 就等于 startIndex + 可视区域能展示的元素个数 :

  • const itemHeight = 60
    const height = 500
    const startIndex = Math.floor(outerContainer.scrollTop / itemHeight)
    const endIndex = startIndex + Math.ceil(height / itemHeight)
    
  • 然后用 startIndex 和 endIndex 去截取数据,并渲染在内层 div 上:

  • const viewData = data.slice(startIndex, endIndex + 1)
    const innerContainer = document.querySelector('.list-container-inner')
    innerContainer.innerHTML = ''
    for(let i = 0; i < viewData.length; i++) {
      const item = document.createElement('div')
      item.innerHTML = viewData[i]
      innerContainer.appendChild(item)
    }
    
  • 最后一步,虽然我们只需要渲染在可视区域上的元素,但是我们还需要给内层的 div 设置 padding-top 和 padding-bottom ,替代那些不需要渲染的元素。由此保证虚拟列表是可以滚动的,而且滚动条位置和列表元素位置是相对应的。 padding-top 等于 startIndex 之前的元素高度和, padding-bottom 等于 endIndex 之后的元素高度和:

  • const paddingTop = startIndex * itemHeight
    const paddingBottom = (data.length - endIndex) * itemHeight
    innerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)
    
  • 完整代码:

  • <style>
      .list-container {
        overflow: auto;
        border: 1px solid black;
        height: 500px;
      }
    </style>
    
    <body>
      <!-- 外部容器用来固定列表容器的高度,同时生成滚动条 -->
      <div class="list-container">
        <!-- 内部容器用来装元素,高度是所有元素高度的和 -->
        <div class="list-container-inner"></div>
      </div>
    
      <script>
        /** --------- 一些基本变量 -------- */
        const itemHeight = 60
        const height = 500
        
        /** --------- 生成数据 -------- */
        const initData = () => {
          const data = []
          for(let i = 0; i < 15; i++) {
            data.push({content: `内容:${i}`, height: itemHeight, color: i % 2 ? 'red' : 'yellow'})
          }
          return data
        }
        const data = initData()
        
        const contentHeight = itemHeight * data.length
        const outerContainer = document.querySelector('.list-container')
        
        const scrollCallback = () => {
          // 获取当前要渲染的元素的坐标
          const scrollTop = Math.max(outerContainer.scrollTop, 0)
          const startIndex = Math.floor(scrollTop / itemHeight)
          const endIndex = startIndex + Math.ceil(height / itemHeight)
          const innerContainer = document.querySelector('.list-container-inner')
          
          // 从data取出要渲染的元素并渲染到容器中
          const viewData = data.slice(startIndex, endIndex + 1)
          innerContainer.innerHTML = ''
          for(let i = 0; i < viewData.length; i++) {
            const item = document.createElement('div')
            const itemData = viewData[i]
            item.innerHTML = itemData.content
            item.setAttribute('style', `height: ${itemData.height}px; background: ${itemData.color}`)
            innerContainer.appendChild(item)
          }
          
          // 未渲染的元素由padding-top和padding-bottom代替,保证滚动条位置正确
          const paddingTop = startIndex * itemHeight
          const paddingBottom = (data.length - endIndex) * itemHeight
          innerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)
        }
        
        // 首屏渲染
        scrollCallback()
        
        // 监听外部容器的滚动事件
        outerContainer.addEventListener('scroll', scrollCallback)
      </script>
    </body>
    

不定高虚拟列表

  • 上面实现的是元素高度固定的虚拟列表,对于元素高度不固定的情况,虚拟列表实现起来会更复杂一些。

  • 因为我们事先不知道元素的高度,所以在监听滚动事件的过程中,需要遍历一遍列表获取每个元素的高度,由此算出 startIndex 、 endIndex 等数据。不过在第一次渲染之前,因为元素没有渲染出来,我们拿不到元素的真实高度。所以这时候需要给元素一个最小高度 itemHeight ,通过 itemHeight 来计算 startIndex 和 endIndex,由此保证第一次渲染出来的元素能占满整个可视区域。

  • 另外,当元素渲染出来以后,我们用一个字典去记录每个元素的真实高度,供下次滚动事件触发时 startIndex 和 endIndex 的计算。

  • startIndex 的算法是在遍历列表元素的过程中,逐步累加当前元素的高度得到 contentHeight ,当第一次出现 contentHeight 大于容器顶部卷起来的长度的时候,说明当前元素是列表可视区域的第一个元素,记为 startIndex。

  • endIndex 的算法是当第一次出现 contentHeight 大于 容器顶部卷起来的长度 + 容器高度 的时候,说明当前元素是列表可视区域的最后一个元素,记为 endIndex 。

  • 完整代码:

  • <style>
      .list-container {
        overflow: auto;
        border: 1px solid black;
        height: 500px;
      }
    </style>
    
    <body>
      <!-- 外部容器用来固定列表容器的高度,同时生成滚动条 -->
      <div class="list-container">
        <!-- 内部容器用来装元素,高度是所有元素高度的和 -->
        <div class="list-container-inner"></div>
      </div>
    
      <script>
        /** --------- 一些基本变量 -------- */
        const itemHeight = 60
        const height = 500
        
        /** --------- 生成数据 -------- */
        const getRandomHeight = () => {
          // 返回 [60, 150] 之间的随机数
          return Math.floor(Math.random() * (150 - itemHeight + 1) + itemHeight)
        }
        const initData = () => {
          const data = []
          for(let i = 0; i < 15; i++) {
            data.push({content: `内容:${i}`, height: getRandomHeight(), color: i % 2 ? 'red' : 'yellow'})
          }
          return data
        }
        const data = initData()
        
        const cacheHeightMap = {}
        const outerContainer = document.querySelector('.list-container')
        
        const scrollCallback = () => {
          let contentHeight = 0
          let paddingTop = 0
          let upperHeight = 0
          let startIndex
          let endIndex
          const innerContainer = document.querySelector('.list-container-inner')
          const scrollTop = Math.max(outerContainer.scrollTop, 0)
          
          // 遍历所有的元素,获取当前元素的高度、列表总高度、startIndex、endIndex
          for(let i = 0; i < data.length; i++) {
            // 初始化的时候因为元素还没有渲染,无法获取元素的高度
            // 所以用元素的最小高度itemHeight来进行计算,保证渲染的元素个数能占满列表
            const cacheHeight = cacheHeightMap[i]
            const usedHeight = cacheHeight === undefined ? itemHeight : cacheHeight
            contentHeight += usedHeight
            if (contentHeight >= scrollTop && startIndex === undefined) {
              startIndex = i
              paddingTop = contentHeight - usedHeight
            }
            if (contentHeight > scrollTop + height && endIndex === undefined) {
              endIndex = i
              upperHeight = contentHeight
            }
          }
          
          // 应对列表所有元素没有占满整个容器的情况
          if (endIndex === undefined) {
            endIndex = data.length - 1
            upperHeight = contentHeight
          }
          
          // 未渲染的元素的高度由padding-top和padding-bottom代替,保证滚动条位置正确
          // 这里如果把设置pading的操作放在渲染元素之后,部分浏览器滚动到最后一个元素时会有问题
          const paddingBottom = contentHeight - upperHeight
          innerContainer.setAttribute('style', `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`)
          
          // 从data取出要渲染的元素并渲染到容器中
          const viewData = data.slice(startIndex, endIndex + 1)
          innerContainer.innerHTML = ''
          const fragment = document.createDocumentFragment()
          for(let i = 0; i < viewData.length; i++) {
            const item = document.createElement('div')
            const itemData = viewData[i]
            item.innerHTML = itemData.content
            item.setAttribute('style', `height: ${itemData.height}px; background: ${itemData.color}`)
            fragment.appendChild(item)
          }
          innerContainer.appendChild(fragment)
          
          // 存储已经渲染出来的元素的高度,供后面使用
          const children = innerContainer.children
          let flag = startIndex
          for(const child of children) {
            cacheHeightMap[flag] = child.offsetHeight
            flag++
          }
        }
        
        // 首屏渲染
        scrollCallback()
        // 监听外部容器的滚动事件
        outerContainer.addEventListener('scroll', scrollCallback)
      </script>
    </body>
    
    
  • 在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/553123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Golang每日一练(leetDay0073) 实现前缀树、最短子数组

目录 208. 实现 Trie (前缀树) Implement-trie-prefix-tree &#x1f31f;&#x1f31f; 209. 长度最小的子数组 Minimum-size-subarray-sum &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每…

多线程的三种创建方式及各自的优缺点分析

第一种方式&#xff1a;继承Thread类&#xff0c;覆写run()方法 1、创建一个MyThread类&#xff0c;继承Thread类&#xff1b; 2、覆写run()方法&#xff0c;在run()方法内编写任务代码&#xff1b; 3、创建MyThread类&#xff0c;需要注意的是&#xff0c;如果想要给线程命名…

传染病学模型 | Matlab实现SI传染病学模型 (SI Epidemic Model)

文章目录 效果一览基本描述模型介绍程序设计参考资料效果一览 基本描述 传染病学模型 | Matlab实现SI传染病学模型 (SI Epidemic Model) 模型介绍 SI传染病模型是一种基于微分方程的流行病学模型,用于模拟传染病在人群中的传播过程。SI模型中,人群被划分为易感者(S)和感染者…

来 Azure 学习 OpenAI 四 - 用 Embedding 赋能 GPT

大家好&#xff0c;我是学生大使 Jambo。在我们前一篇文章中&#xff0c;我们介绍了 OpenAI 模型的调用。今天&#xff0c;我将为大家介绍 Embedding 的使用。 嵌入是什么 嵌入&#xff08;Embedding &#xff09;是一种将高维数据映射到低维空间的方法。嵌入可以将高维数据可…

第一章 初识Python

1.1 课前必读 课程大纲 1.2 Python介绍 Python特点&#xff1a; 主流语言&#xff1b;用途广泛&#xff0c;号称万能语言&#xff1b;上手简单&#xff1b; Python用途&#xff1a; 数据挖掘&#xff08;爬虫&#xff09;和数据分析自动化脚本编写&#xff08;软件测试人员使用…

尚硅谷周阳老师 SpringCloud第二季学习笔记

前言&#xff1a;首先感谢尚硅谷周阳老师的讲解&#xff0c;让我对springcloud有了很好的理解&#xff0c;周阳老师的讲课风格真的很喜欢&#xff0c;内容充实也很幽默&#xff0c;随口一说就是一个段子&#xff0c;我也算是周阳老师的忠实粉丝啦。 先说说课程总体内容 以下是…

[学习笔记] [机器学习] 6. [上]决策树算法(熵Entropy、信息增益(率)、基尼值(指数)、CART剪枝、特征工程特征提取、回归决策树)

视频链接数据集下载地址&#xff1a;无需下载 学习目标&#xff1a; 掌握决策树实现过程知道信息熵的公式以及作用知道信息增益、信息增益率和基尼指数的作用知道id3、c4.5、cart算法的区别了解cart剪枝的作用知道特征提取的作用应用DecisionTreeClassifier实现决策树分类 1…

开放原子训练营(第三季)inBuilder低代码开发实验室,低代码到底该长什么样

目录 前言&#xff1a; 一、什么是inBuilder低代码开发实验室 二、技术特征 2.1开放性 2.2开发语言无关性 2.3云原生 2.4模型工程化 2.5全栈模型刻画 2.6运行态定制 2.7仓库介绍 三、快速入门 四、实操案例 4.1定义数据源 4.2 设计页面 4.3发布调试 五、总结 前言&#xf…

Activiti实战——Springboot整合Activiti

目录 一、Activiti数据库表名说明 二、Spring boot整合activiti 1. 创建springboot项目 2. 引入activiti依赖及项目依赖 3. 配置数据源 &#xff08;1&#xff09;创建数据源配置文件 &#xff08;2&#xff09;配置文件 4. 配置Acitviti引擎 5. 启动项目 三、Activiti…

【MySQL 数据库】1、MySQL 的 DDL、DML、DQL 语句

目录 一、MySQL 应该掌握哪些知识点 &#xff1f;二、数据库相关概念三、主流关系型数据库管理系统四、关系型数据库五、SQL 语句的分类六、DDL(1) 数据库操作(2) 表操作(3) 字段的数据类型(4) 创建员工表(5) 修改表结构(6) 删除某一张表 七、DML八、DQL(1) 员工表(2) distinct…

经典神经网络(4)Nin-Net及其在Fashion-MNIST数据集上的应用

经典神经网络(4)Nin-Net及其在Fashion-MNIST数据集上的应用 1 Nin-Net的简述 1.1 Nin-Net的概述 LeNet、AlexNet和VGG都有⼀个共同的设计模式&#xff1a;通过⼀系列的卷积层与汇聚层来提取空间结构特征&#xff1b;然后通过全连接层对特征的表征进⾏处理。AlexNet和VGG对Le…

线程池的创建与使用

void execute(Runnable run)方法处理Runnbale任务 Future<> submit(Callable<> task)方法处理Callable任务 void shutdown()结束线程池 List<\Runnable> shutdownNow()立即结束线程池&#xff0c;不管任务是否执行完毕 //创建线程池的一种方式 ExecutorServi…

基于WebApi实现ModbusTCP数据服务

在上位机开发过程中&#xff0c;有时候会遇到需要提供数据接口给MES或者其他系统&#xff0c;今天跟大家分享一下&#xff0c;如何在Winform等桌面应用程序中&#xff0c;开发WebApi接口&#xff0c;提供对外数据服务。 为了更好地演示应用场景&#xff0c;本案例以读取Modbus…

Leetcode 209. 长度最小的子数组——go语言实现

文章目录 一、题目描述二、代码实现方法一&#xff1a;暴力法解题思路代码实现复杂度分析 方法二&#xff1a;滑动窗口 双指针解题思路代码实现复杂度分析 方法三&#xff1a;前缀和 二分查找解题思路代码实现复杂度分析 一、题目描述 给定一个含有 n 个正整数的数组和一个正…

STM32 10个工程篇:1.IAP远程升级(四)

在前三篇博客中主要介绍了IAP远程升级的应用背景、下位机的实现原理、以及基于STM32CubeMX对STM32F103串口DMA的基本配置&#xff0c;第四篇博客主要想介绍Labview端上位机和下位机端的报文定义和通信等。 当笔者工作上刚接触到STM32 IAP升级的时候&#xff0c;实事求是地说存在…

【科普】电压和接地真的存在吗?如何测试?

经常在实验室干活的&#xff0c;难免不被电击过&#xff0c;尤其是在干燥的北方&#xff0c;“被电”是常有的事情&#xff0c;我记得有一次拿着射频线往仪表上拧的时候&#xff0c;遇到过一次严重的电火花&#xff0c;瞬间将仪表的射频口边缘烧出了一个疤&#xff0c;实验室遭…

LeetCode83. 删除排序链表中的重复元素

写在前面&#xff1a; 题目链接&#xff1a;LeetCode83. 删除排序链表中的重复元素 编程语言&#xff1a;C 题目难度&#xff1a;简单 一、题目描述 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 …

Java中异常的处理及捕获

Java中异常的处理及捕获 一、异常的概述 &#xff08;1&#xff09;Java中异常的作用&#xff1a;增强程序的健壮性 &#xff08;2&#xff09;在Java中所有的Error&#xff08;错误&#xff09;和异常&#xff08;Exception&#xff09;都继承了同一个父类Throwable 二、异…

postgresql内核源码分析-删除表drop table流程

专栏内容&#xff1a;postgresql内核源码分析个人主页&#xff1a;我的主页座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物&#xff0e; 目录 前言 调用关系 概要流程 详细流程 创建对象列表空间 删除多个指定的数据库…

【蓝桥杯国赛真题27】Scratch LED屏幕 少儿编程scratch图形化编程 蓝桥杯国赛真题讲解

目录 scratch LED屏幕 一、题目要求 编程实现 二、案例分析 1、角色分析