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

news2024/11/25 0:43:25

用JS实现虚拟列表

简介

  • 当一个列表需要渲染大量数据的时候是非常耗时的,而且在列表滚动的过程中会出现卡顿的现象。即使用上懒加载解决了列表初始化时渲染过慢的问题,但是每次拉取下一页数据的时候都会造成列表的重新渲染。随着拉取的数据越来越多,列表渲染时间长、卡顿的问题依然存在。

  • 这个时候虚拟列表就派上用场了。虚拟列表的实现原理简单来说,就是列表并不会把所有的数据都渲染出来,而是通过监听滚动事件然后实时计算当前是哪几条数据显示在页面上,然后只渲染用户可以看见的这几条数据。

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBJKlpWY-1683592680154)(C:\Users\quyanliang\AppData\Roaming\Typora\typora-user-images\1683592322292.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/534886.html

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

相关文章

使用火焰图进行性能分析(一)

为什么会用到火焰图&#xff1f;火焰图能干那些事儿&#xff1f; 分析函数执行的频度&#xff1b;分析哪些函数经常阻塞&#xff1b;分析哪些函数频繁操作内存&#xff1b; 火焰图的主要特点&#xff1a; 每一列代表一个调用栈&#xff0c;每个格子代表一个函数&#xff1b;…

计算机图形学-GAMES101-4

一、变换矩阵中的旋转部分 当我们旋转Q角度和旋转-Q角度时&#xff0c;变换矩阵中旋转的部分如下图所示&#xff1a; 旋转Q和旋转-Q的变换矩阵应该互为逆矩阵&#xff0c;而我们可以看到它们互为对方矩阵的转置。其实Rq是一个正交矩阵&#xff0c;因此其逆矩阵就是它自己的转…

chrome渲染引擎的工作主流程

一见如故 浏览器的渲染&#xff1a;HTML字符串>渲染成最终的像素1、CSS Parser发生在css预解析线程中&#xff0c;不在主线程中&#xff1b;会预览整个HTML文档&#xff0c;下载css相关全部内容&#xff0c;解析生成CSSOM树 2、attachment >以及生成布局树>分层>按…

微服务的使用场景和架构设计方案

目录 【单体架构】 【微服务解决哪些问题】 微服务的拆分原则 微服务使用过程中有哪些坑&#xff1f; 【RPC框架】 常见的网络 IO 模型 RPC 执行过程总结 【CAP原理】 如何使用 CAP 理论 【服务注册和发现】 【配置中心】 【Consul】 Consul介绍 Consul角色 Con…

Go语言的结构体、方法、指针

目录 【定义新数据类型】 【结构体】 定义结构体 结构体变量的声明和初始化 结构体的内存表示 【方法】 receiver 参数 receiver参数的约束 方法的深入理解 goroutine中方法的使用 receiver 参数类型如何选择&#xff1f;T还是*T&#xff1f; 方法集合 【指针】 …

9. 三星索引和Mysql内核查询成本计算实战

MySQL性能调优 1. 高性能的索引创建策略1.1 只为用于搜索、排序或分组的列创建索引1.2 合理设计多列索引1.3 尽可能设计三星索引1.4 主键尽量是很少改变的列1.5 处理冗余和重复索引1.6 删除未使用的索引1.7 InnoDB中的索引 2. 补充资料&#xff1a;磁盘和B树Mysql内核查询成本计…

从0到1无比流畅的React入门教程

无比流畅的React入门教程TOC React 是什么 简介 用于构建 Web 和原生交互界面的库React 用组件创建用户界面通俗来讲&#xff1a;是一个将数据渲染为HTML视图的开源JS库 其他信息 Facebook 开发&#xff0c;并且开源 为什么使用React? 原生JS使用DOM-API修改UI代码很繁…

使用Appium实现录制回放

1、cmd中转到abd所在位置&#xff1a; cd C:\Users\lenovo\AppData\Local\Android\Sdk\platform-tools 2、打开Appium运行 3、打开Appium Inspector &#xff08;1&#xff09;获取设备名称 在cmd中输入以下命令&#xff1a; adb devices &#xff08;2&#xff09;获取appP…

c++核心知识—多态

目录 一、多态 1、多态的基本概念 2、深入剖析多态原理&#xff1a; 3、纯虚函数和抽象类 4、虚析构和纯虚析构 一、多态 1、多态的基本概念 多态是C面向对象三大特性之一 多态分为两类&#xff1a; 1、静态多态: 函数重载 和 运算符重载 属于静态多态&#xff0c;复用…

前端八股文(二)

1.什么是diff算法&#xff1f; https://www.bilibili.com/video/BV1JR4y1R7Ln/?spm_id_from333.337.search-card.all.click&vd_source0406fa5cf8203ba41f1c8aec5f967e9d 我们修改了文本内容后会生成新的虚拟dom,新旧俩个虚拟dom之间是存在一定差异的&#xff0c;如果能快…

未来的航空电子设备会是什么样子?

具有多功能航空电子设备、大触摸屏显示器、先进通信系统、高性能/低功耗解决方案和人工智能 (AI) 功能的驾驶舱将成为未来军事飞行员日常生活的一部分。 如今&#xff0c;配备模拟驾驶舱的军用飞机已经很少见&#xff0c;因为大多数都已被采用先进嵌入式硬件和软件解决方案的现…

金融学学习笔记第2章

第2章 金融市场和金融机构 一、金融体系 金融体系包括金融市场、中介、服务公司和其它用于执行家庭、企业及政府的金融决策的机构 1.金融市场 金融市场&#xff1a;以金融资产为交易对象而形成的供求关系及其机制的总和 金融市场可分为有特定地理位置的市场和没有特定地点的市…

使用NodeJs创建Web服务器

Web服务器 什么是Web服务器&#xff1f; 当应用程序&#xff08;客户端&#xff09;需要某一个资源时&#xff0c;可以向一个台服务器&#xff0c;通过Http请求获取到这个资源&#xff1b;提供资源的这个服务器&#xff0c;就是一个Web服务器&#xff1b; 目前有很多开源的We…

用ChatGPT生成一个Python贪吃蛇游戏(42)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 什么是ChatGPT&#xff1f; ChatGPT可以帮我们做什么&#xff1f; 用ChatGPT生成一个Python贪吃蛇游戏。 什么是C…

Rust每日一练(Leetday0002) 中位数、回文子串、Z字形变换

目录 4. 寻找两个正序数组的中位数 Median of two sorted arrays &#x1f31f;&#x1f31f;&#x1f31f; 5. 最长回文子串 Longest Palindromic Substring &#x1f31f;&#x1f31f; 6. Z字形变换 Zigzag Conversion &#x1f31f;&#x1f31f; &#x1f31f; 每日…

C/C++每日一练(20230517) 排序问题、查找小值、寻找峰值

目录 1. 排序问题 &#x1f31f; 2. 查找小值 &#x1f31f; 3. 寻找峰值 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 排序问题 输入10个数&#…

RK3588平台开发系列讲解(进程篇)Linux文件系统数据结构

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、Linux 文件系统数据结构有哪些二、超级块结构 spuer_block三、目录 dentry四、文件索引结点 inode五、打开的文件 file沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇将介绍 Linux 文件系统数据结构…

深入理解MySQL中的事务和锁

目录 数据库中的事务是什么&#xff1f; MySQL事务的隔离级别 脏读、不可重复读、幻读 MVCC&#xff08;多版本并发控制&#xff09; 快照读和当前读 MySQL中的锁 MyISAM引擎的锁&#xff1a; InnoDB引擎的锁&#xff1a; 乐观锁和悲观锁 共享锁和排他锁 数据库中的事…

【STL二十】算法——排序操作(sort、stable_sort)_集合操作(merge)

【STL二十】算法——排序操作(sort、stable_sort&#xff09;_ 集合操作(merge&#xff09; 一、分类二、修改序列的操作三、排序操作1、sort2、stable_sort3、is_sorted、is_sorted_until 四、集合操作1、merge2、inplace_merge 一、分类 根据网站https://www.apiref.com/cpp…

JavaScript实现输出一个“天”字的代码

以下为实现输出一个“天”字的程序代码和运行截图 目录 前言 一、实现输出一个“天”字 1.1 运行流程及思想 1.2 代码段 1.3 JavaScript语句代码 1.4 运行截图 前言 1.若有选择&#xff0c;您可以在目录里进行快速查找&#xff1b; 2.本博文代码可以根据题目要求实现相…