如何实现虚拟列表?定高和不定高两种场景

news2024/12/24 10:10:27

之前我写了一篇文章:如何使用 IntersectionObserver API 来实现数据的懒加载 在文章的最后,我们提到如果加载的列表数据越来越多,我们不可能把所有的数据都渲染出来,因为这样会导致页面卡住甚至崩溃。

为了优化这种长列表场景,我们可以使用虚拟列表,核心思想是:仅渲染可视区域内(及其附近)的列表项。即不管列表有多少条数据,只取指定数量的项渲染,比如说 15 条,15 条足以覆盖可视区域以及其上下附近区域,当然这个数量视具体情况决定。

定高场景

定高的意思是我们提前知道每个列表项的高度,比如 100px。
假设我们现在有 10000 条数据,当用户上下滚动的时候,始终取对应的 15 条数据渲染。代码以 vue3 来示例。

思路分析

  • 定义 DOM 结构和滚动事件
    在这里插入图片描述
    在上述代码中,我们定义三层 DOM 结构,最外层是视口 viewport,次外层是所有列表项的父容器(这里设置其高度为整个列表内容的高度:style=“{ height: ${contentHeight}px }”),最里层是通过 v-for 循环生成的列表项。然后我们在视口层监听 scroll 事件。

  • 设置样式
    在这里插入图片描述
    这里视口的高度设置为 800px,列表项 content-item 的高度设置为 100px,且定位设为绝对定位,这很关键,因为上下滚动的时候每个列表项距离顶部的距离需要通过 top 这个定位属性来设置。

  • 渲染数据的截取
    我们整个列表 items 有 10000 条数据,当用户上下滚动的时候,我们需要知道当前位置需要截取列表中的哪 15 条数据?所以我们需要知道截取开始的位置 startIndexstartIndex 是动态变化的,它需要根据用户的滚动位置来计算。

    scrollTop 属性可以获取滚动条距离内容顶部的距离 scrollTop,scrollTop 是由 content-item 的高度撑起来的,那么 startIndex = scrollTop/100px(content-item) ,那么我们当前需要渲染的数据就是:renderItems = items.slice(startIndex, startIndex+15)

    具体代码如下:
    在这里插入图片描述
    在上述代码中,我们在使用 Array.form 方法生成列表数据的时候,每个 item 都设置了 top 属性,top = i * ITEM_HEIGHT,即第一个 item 距离顶部的距离为 0,第二个 item 距离顶部的距离为 100px,第三个 item 距离顶部的距离为 200px……,在 DOM 上设置 style::style=“{ top : ${item.top}px}” 可保证每个 item 处于正确的位置。

运行代码,上下滚动,可得到如下表现:

虚拟列表定高

在视频中我们可以看到,无论我们怎么滑动列表,最终都只会渲染 15 个列表项。

完整代码如下:

<template>
  <div 
    ref="viewportRef"
    class="viewport"
    @scroll="handleScroll"
  >
    <div 
      class="content-wrap"
      :style="{ height: `${contentHeight}px` }"
    >
      <div 
        v-for="item in renderItems"
        :key="item.id"
        :style="{ top : `${item.top}px`}"
        class="content-item"
      >
        <p>{{ item.text }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, ref } from 'vue';

const ITEM_HEIGHT = 100; // 假设每个 item 的高度为 100px
const RENDER_SIZE = 15; // 假设每次渲染 15 条数据
// 假设 10000 条数据
const items = Array.from({ length: 10000 }, (v, i) => {
  return {
    id: i,
    text: `item-${i+1}`,
    top: i * ITEM_HEIGHT
  };
});

const startIndex = ref(0);
const viewportRef = ref(null);

const renderItems = computed(() => {
  const endIndex = startIndex.value + RENDER_SIZE;
  return items.slice(startIndex.value, endIndex);
});
// 整个列表的高度
const contentHeight = computed(() => {
  return items.length * ITEM_HEIGHT;
});

const handleScroll = () => {
  const scrollTop = viewportRef.value?.scrollTop;
  // 更新 startIndex
  startIndex.value = Math.floor(scrollTop / ITEM_HEIGHT);
}
</script>

<style>
.viewport {
  height: 800px;
  overflow-y: auto;
}
.content-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.content-item {
  height: 100px; /* 假设每个项目高度为100px */
  position: absolute;
  width: 100%;
  border: 1px solid #000;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

不定高场景

不定高是我们不能提前知道每个列表项的高度,它的高度是动态变化的,具体多高由它的数据的多少决定,有些项数据比较多,那么它最终渲染出来的 DOM 高度就比较高。这种情况就需要我们动态去计算每个列表项的高度。

思路分析

  • 定义 DOM 结构和滚动事件
    在这里插入图片描述
    这里最外层视口层的结构和之前定高的情况是一样的,次外层有两个 DOM:一个是 content-placeholder ,用于占位,它的高度就是整个列表渲染后的高度。另一个是 content-wrap ,是当前渲染列表的父容器,注意它的样式::style=“{ transform: translateY(${offset}px) }”,offset 的值是动态计算的,为的是确保在滚动的过程中,当前渲染的列表处于整个列表中正确的位置。

    最里层是当前渲染的列表,注意,我们这里的 ref 使用了函数 :ref=“(el) => renderItemsRef(el, item.id)”renderItemsRef 函数中把已渲染的列表项真实的高度存下来,用于后续的计算。

  • 设置样式
    在这里插入图片描述
    占位元素是绝对定位的

  • 渲染数据的截取
    不定高的情况下要确认 startIndex 要比定高的情况复杂得多。
    同定高的情况一下,我同样需要 scrollTop 来用于 startIndex 的计算,观察下图:
    在这里插入图片描述
    观察上图得知,当前视口渲染的第一个列表项 curList1 在整个列表中的位置即为 startIndex,我们可以根据滚动条的位置(scrollTop)来计算,也就是说,我们需要确定在 curList1 之前有几个列表项,我们从索引 0 开始遍历整个列表,把每个列表项的高度相加,当 totalHeight >= scrollTop 的时候,我们就遍历到了 curList1 这个列表项,那么当前的索引 index 就是我们需要的 startIndex,具体代码如下:
    在这里插入图片描述
    上述代码中,allItems 是整个列表数据,具体如下:
    在这里插入图片描述
    allItems 这里我们设置了一个随机高度,模拟不定高的情况,但在实际场景中,height 应该设置为一个接近于 item 渲染后的真实高度,即这里的 ITEM_HEIGHT

    同时 hasRenderedItemsHeight 就是已经渲染的列表项的高度的集合,它是一个对象,key 是列表项,value 是列表项渲染后的高度,具体的赋值代码如下,也就是我们前面提到的 renderItemsRef 函数:
    在这里插入图片描述
    在上述代码中,每次有新的列表项渲染完成,我们都需要调用 updateRenderTotalHeight 函数去更新整个列表的实际高度。

    最后,我们在组件挂载的时候和滚动事件触发的时候调用 updateRenderItems 即可实现不定高的虚拟列表
    在这里插入图片描述

    运行代码,可得到如下表现:

虚拟列表不定高

完整代码如下:

<template>
  <div 
    ref="viewportRef"
    class="viewport"
    @scroll="handleScroll"
  >
    <div class="content-placeholder" :style="{ height: `${renderTotalHeight}px` }"></div>
    <div 
      class="content-wrap"
      :style="{ transform: `translateY(${offset}px)` }"
    >
      <div 
        v-for="item in renderItems"
        :ref="(el) => renderItemsRef(el, item.id)"
        :key="item.id"
        :style="{ height: `${item.height}px`}"
        class="content-item"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from 'vue';

const RENDER_SIZE = 15; // 假设每次渲染 15 条数据
const ITEM_HEIGHT = 100; // 假设每个 item 真实渲染后高度接近 100px
// 假设 10000 条数据
const allItems = Array.from({ length: 10000 }, (v, i) => {
  return {
    id: i,
    text: `item-${i+1}`,
    // 设置随机高度, 在实际项目中应该根据 item 的实际情况设置一个接近于 item 渲染后的高度
    height: Math.floor(Math.random() * 100) + 50
  };
});

const viewportRef = ref(null);
const renderItems = ref([]); // 当前需要渲染的 item
const renderTotalHeight = ref(0); // 整个已渲染列表的高度
const hasRenderedItemsHeight = ref({}); // 已渲染的 item 数据 height 
const offset = ref(0);

const updateRenderItems = () => {
  const scrollTop = viewportRef.value?.scrollTop;
  
  let startIndex = 0;
  let startOffset = 0;

  for (let i = 0; i < allItems.length; i++) {
    const h = hasRenderedItemsHeight.value[allItems[i].id] || ITEM_HEIGHT;
    startOffset += h;
    if (startOffset >= scrollTop) {
      startIndex = i;
      break;
    }
  }

  renderItems.value = allItems.slice(startIndex, startIndex + RENDER_SIZE);
  offset.value = startOffset - hasRenderedItemsHeight.value[allItems[startIndex].id];
  
}

const renderItemsRef = (el, id) => {
 if (el) {
  // 存放已渲染的 item 的高度
  hasRenderedItemsHeight.value[id] = el.offsetHeight;
  // 更新容器的高度
  nextTick(updateRenderTotalHeight);
 }
}

const updateRenderTotalHeight = () => {
  renderTotalHeight.value = allItems.reduce((sum, item) => sum + (hasRenderedItemsHeight[item.id] || ITEM_HEIGHT), 0);
}

const handleScroll = () => {
  updateRenderItems();
}

onMounted(() => {
  updateRenderItems();
})
</script>

<style>
.viewport {
  height: 800px;
  overflow-y: auto;
  position: relative;
}
.content-placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
}

.content-item {
  width: 100%;
  border: 1px solid #000;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

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

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

相关文章

英伟达SSD视觉算法,jetson.inference在jetson nano中部署

一、用官方镜像刷机 安装SD卡擦除工具SD Card Formatter https://www.sdcardformatter.com/download/ 格式化SD卡 下载官方镜像 https://developer.nvidia.com/jetson-nano-sd-card-image 安装刷机工具balenaEtcher https://www.balena.io/etcher 将上面下载的镜像压缩包解…

[leetcode hot 150]第五十六题,合并区间

题目&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 思路&#xff1a; 这道题目要求合并一…

若依跳转(新增)页面,在菜单中不显示的页面

在router.js文件中 跳转方式 this.$router.push(/monitor/b/b)

Java面试八股之++操作符是线程安全的吗

操作符是线程安全的吗 操作符本身在Java中并不是线程安全的。这个操作实际上包含三个步骤&#xff1a;读取变量的值、将值加1、然后将新值写回内存。在多线程环境下&#xff0c;如果多个线程同时对同一个变量执行操作&#xff0c;就可能出现竞态条件&#xff08;race conditio…

数据结构(七)递归、快速排序

文章目录 一、递归&#xff08;一&#xff09;使用递归实现1~n求和1. 代码实现&#xff1a;2. 调用过程&#xff1a;3. 输出结果&#xff1a; &#xff08;二&#xff09;青蛙跳台阶问题1. 问题分析2. 代码实现3. 输出结果4. 代码效率优化5. 优化后的输出结果 二、快速排序&…

STM32系列-STM32介绍

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” STM32介绍 STM32介绍 ST&#xff1a;指的是意法半导体 M&#xff1a;指定微处理器 32&#xff1a;表示计算机处理器位数 ARM分成三个系列&#xff1a; Cortex-A&#xff1…

PostgreSQL数据库提权

前面讲述了mysql、SqlServer、Redis数据库相关的提权方式&#xff0c;有兴趣的也可以去看看。 这里讲的postgreSQL数据库提权就是任意命令执行漏洞(CVE-2019-9193)。 目录 数据库简介 漏洞原理 影响版本 漏洞利用 利用前提 漏洞复现 复现准备 复现过程 漏洞修复 数据…

基于C#开发web网页管理系统模板流程-主界面管理员入库和出库功能完善

前言 紧接上篇->基于C#开发web网页管理系统模板流程-主界面管理员录入和编辑功能完善-CSDN博客 本篇将完善主界面的管理员入库和出库功能&#xff0c;同样的&#xff0c;管理员入库和出库的设计套路适用于动态表的录入和编辑 首先还是介绍一下本项目将要实现的功能 &#xf…

【aI】LiveKit Agents Playground

demo 是跑在 playground中的。 语音助手demo 可以语音对话 概念 Concepts Agent: A function that defines the workflow of a programmable, server-side participant. This is your application code. Worker: A container process responsible for managing job queuing …

业内宝刊!影响因子3连涨,OA可选,Elsevier旗下这本SSCI解救你的选刊纠结症

【SciencePub学术】今天小编给大家带来了一本经济类的高分优刊解读&#xff0c;隶属于Elsevier出版社&#xff0c;JCR1区&#xff0c;中科院2区&#xff0c;影响因子高达4.8&#xff0c;且实时影响因子还在持续上涨中&#xff0c;领域相符的学者可着重考虑&#xff01; Emergin…

使用 Flask 实现异步请求处理

文章目录 为什么需要异步请求处理&#xff1f;在 Flask 中实现异步请求处理使用 Flask-Cors 扩展 总结 在开发 Web 应用程序时&#xff0c;异步请求处理是提高性能和并发能力的重要方法之一。Flask 是一个轻量级的 Web 框架&#xff0c;它提供了易于使用的工具来实现异步请求处…

【CCF-CSP】202309-1 202309-2 坐标变换

坐标变换&#xff08;其一&#xff09; 代码&#xff1a; #include <bits/stdc.h> using namespace std; int main(){int n,m,x,y,sumx0,sumy0;cin>>n>>m;for(int i1;i<n;i){cin>>x>>y;sumxx,sumyy;}for(int i1;i<m;i){cin>>x>&…

深入解析三层架构:构建稳定高效的软件系统

概述 顾名思义&#xff0c;三层架构分为三层&#xff0c;分别是“数据访问层”、“业务逻辑层”、“表示层”。 数据访问层&#xff1a;数据访问层在作业过程中访问数据系统中的文件&#xff0c; 实现对数据库中数据的读取保存操作。 表示层&#xff1a;主要功能是 显示数据和…

易查分小程序 学生成绩管理小程序

亲爱的老师们&#xff0c;是不是每次成绩公布后&#xff0c;家长们的连环夺命call让你头大&#xff1f;担心孩子们的成绩信息安全&#xff0c;又想快速分享给家长&#xff0c;这可咋整&#xff1f;别急&#xff0c;易查分小程序来帮忙啦&#xff01; 安全有保障 智能验证码&a…

Java+IDEA+SpringBoot药物不良反应ADR智能监测系统源码 ADR智能化监测系统源码

JavaIDEASpringBoot药物不良反应ADR智能监测系统源码 ADR智能化监测系统源码 药物不良反应&#xff08;Adverse Drug Reaction&#xff0c;ADR&#xff09;是指在使用合格药品时&#xff0c;在正常的用法和用量下出现的与用药目的无关的有害反应。这些反应往往因药物种类、使用…

Vite + Vue3 + Electron 创建打包桌面程序

10 【Vite Vue3 Electron 创建打包桌面程序】 1.使用 Vite 构建 Electron 项目 1.1 创建 Vite 应用&#xff0c;安装 Electron 依赖 创建一个 Vite 项目 npm init vitelatest安装 Electron 相关依赖 npm install electron -D npm install vite-plugin-electron -D 1.2 在…

网络工程师---第四十三天

1、网络地址转换请简述DNS服务器迭代查询与递归的区别&#xff1f; 2、请从技术方面简述RAIDO、RAID1、RAID3、 RAID5的特点&#xff1f; 3、请从层次结构、部署设备和功能配置方面描述层次化的网络结构&#xff1f; 4、请简述IPSECVPN和AH和ESP的区别&#xff1f; 5、请简述ID…

5G工业数采网关是什么?天拓四方

随着工业4.0时代的到来&#xff0c;数字化、网络化、智能化成为工业发展的新趋势。在这个过程中&#xff0c;5G工业数采网关作为一种关键设备&#xff0c;发挥着越来越重要的作用。本文将详细解析5G工业数采网关是什么&#xff0c;以及它在工业领域中的应用和重要性。 一、5G工…

Android Graphics图形栈SurfaceFlinger之间各种Layer以及对应Buffer之间的关系

Android Graphics图形栈SurfaceFlinger之间各种Layer以及对应Buffer之间的关系 SurfaceFlinger layer之间的对应关系

云衔科技:为什么推荐使用zoho crm客户管理系统?

在当今快速变化的商业环境中&#xff0c;企业对高效、智能化的客户关系管理&#xff08;CRM&#xff09;系统的需求日益增长。Zoho CRM&#xff0c;作为全球领先的企业级CRM解决方案提供商&#xff0c;凭借其全面的功能、高度的可定制性、以及无缝集成的生态系统&#xff0c;成…