当前位置:首页 » 《休闲阅读》 » 正文

前端虚拟滚动列表 vue虚拟列表

20 人参与  2024年11月15日 14:02  分类 : 《休闲阅读》  评论

点击全文阅读


文章目录

前端虚拟滚动列表(方法一:利用IntersectionObserver api 简单)前端虚拟滚动列表(方法二:监听滚动计算 麻烦)实现效果:实现思路:具体代码 替代方案vue-virtual-scrollervue-virt-listvue-draggable-virtual-scroll-listvirtual-list自己找吧,我就不一一列举了,看图

方法一利用浏览器原生api去实现,可以实现不等高的列表虚拟滚动,intersectionObserver 多用于图片懒加载,虚拟滚动列表
方法二通过监听滚动条的位置,去计算显示的内容,这里需要列表等高,当然不等高也可以计算,稍微改改


前端虚拟滚动列表(方法一:利用IntersectionObserver api 简单)

IntersectionObserver可以用来自动监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"
IntersectionObserver 方案多用于图片懒加载或者列表虚拟滚动

IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数: callback:可见性发现变化时的回调函数 option:配置对象(可选)。
构造函数的返回值是一个观察器实例。实例一共有4个方法:

observe:开始监听特定元素

unobserve:停止监听特定元素

disconnect:关闭监听工作

takeRecords:返回所有观察目标的对象数组

callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback。
callback一般会触发两次。一次是目标元素刚刚进入视口,另一次是完全离开视口。

const io = new IntersectionObserver((changes, observer) => {  console.log(changes);  console.log(observer);});
options threshold: 决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。root: 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素rootMargin: 用来扩大或者缩小视窗的的大小,使用css的定义方法,10px 10px 30px 20px表示top、right、bottom 和 left的值
————————————————

这里是后面补充的简单还原了下面方法二的例子,重点在60行,从哪儿看就可以

<template>  <div class="big-box">    <div class="download-box txt" id="scrollable-div">      <div v-for="(item, index) in props.seqText" :key="index" class="line-box">        <template v-if="index === 0 && start === 0">          <div :class="{ 'text-title': props.collapsed, 'text-title-samll': !props.collapsed }">            {{ item }}          </div>        </template>        <template v-else>          <div :class="{ 'text-number': props.collapsed, 'text-number-samll': !props.collapsed }">            {{ calLine(item, index + start) }}          </div>          <div            :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }"            :data="item"          >            ''          </div>          <div :class="{ 'text-number2': props.collapsed, 'text-number2-samll': !props.collapsed }">            {{ endRow(item, index + start) }}          </div>        </template>      </div>    </div>  </div>  <SearchBox :againFind="againFind" /></template><script lang="ts" setup>import { watch, onMounted, PropType, reactive, ref } from 'vue';import SearchBox from '/@/components/SearchBox/index.vue';import { message } from 'ant-design-vue';const props = defineProps({  collapsed: {    type: Boolean,    default: true,  },  seqText: {    type: Array as PropType<string[]>,    default: [''],  },});let width = 100;const geneTexts: Array<string> = [];const data = reactive({  geneTexts,});const calLine = (item: any, index: number) => {  return width * (index - 1) + 1;};const endRow = (item: any, index: number) => {  return width * index;};//  这里是核心要点const io = new IntersectionObserver(  (entries) => {    console.log(entries);    for (const entry of entries) {      if (entry.isIntersecting) {        const elTxt = entry.target;        // console.log(elTxt.getAttribute('data'));        elTxt.innerHTML = elTxt.getAttribute('data');        io.unobserve(elTxt);      }    }  },  {    root: document.getElementById('scrollable-div'),    // rootMargin: 0,    threshold: 0.5,  },);setTimeout(() => {  const elList = document.querySelectorAll('.text-box');  console.log(elList);  elList.forEach((element) => {    io.observe(element);  });}, 1000);const againFind = ref(1);let start = ref(0);</script><style lang="less" scoped>// @import '/@/assets/styles/views/medaka.less';.big-box {  background: #282c34;  padding: 30px 20px;  height: 870px;}.download-box {  width: 100%;  // padding: 0px 20px;  // outline: 1px solid rgb(17, 0, 255);  overflow: hidden;  .line-box {    .flex-type(flex-start);    height: 30px;  }  &.txt {    background: #282c34;    color: #fff;    height: 810px;    overflow: auto;    .el-row {      display: flex;      align-items: center;      margin-bottom: 10px;      margin: auto;      font-size: 22px;    }  }}@media screen and (min-width: 1842px) {  .text-box-samll {    letter-spacing: 1.5px;    font-size: 15px;  }  .text-number-samll {    min-width: 60px;    font-size: 15px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 60px;    font-size: 15px;  }  .text-title-samll {    font-size: 15px;  }  .text-box {    font-size: 22px;    // letter-spacing: 3px;  }  .text-number {    min-width: 100px;    font-size: 22px;  }  .text-number2 {    margin-left: 20px;    min-width: 100px;    font-size: 22px;  }  .text-title {    font-size: 22px;  }}@media screen and (min-width: 1600px) and (max-width: 1841px) {  .text-box-samll {    font-size: 15px;  }  .text-number-samll {    min-width: 40px;    font-size: 15px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 15px;  }  .text-title-samll {    font-size: 15px;  }  .text-box {    font-size: 20px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 20px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 20px;  }  .text-title {    font-size: 20px;  }}@media screen and (min-width: 1443px) and (max-width: 1599px) {  .text-box-samll {    font-size: 13px;  }  .text-number-samll {    min-width: 40px;    font-size: 13px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 13px;  }  .text-title-samll {    font-size: 13px;  }  .text-box {    font-size: 18px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 15px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 18px;  }  .text-title {    font-size: 18px;  }}@media screen and (max-width: 1442px) {  .text-box-samll {    font-size: 11px;  }  .text-number-samll {    min-width: 40px;    font-size: 11px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 11px;  }  .text-title-samll {    font-size: 11px;  }  .text-box {    font-size: 16px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 15px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 16px;  }  .text-title {    font-size: 16px;  }}</style>

前端虚拟滚动列表(方法二:监听滚动计算 麻烦)

在大型的企业级项目中经常要渲染大量的数据,这种长列表是一个很普遍的场景,当列表内容越来越多就会导致页面滑动卡顿、白屏、数据渲染较慢的问题;大数据量列表性能优化,减少真实dom的渲染


看图:绿色是显示区域,绿色和蓝色中间属于预加载:解决滚动闪屏问题;大致了解了流程在往下看;
在这里插入图片描述

实现效果:

先说一下你看到这么多真实dom节点是因为做了预加载,减少滚动闪屏现象,这里写了300行,可以根据实际情况进行截取
在这里插入图片描述

实现思路:

虚拟列表滚动大致思路:两个div容器

  外层:外部容器用来固定列表容器的高度,同时生成滚动条  内层:内部容器用来装元素,高度是所有元素高度的和  外层容器鼠标滚动事件  dom.scrollTop 获取滚动条的位置  根据每行列表的高以及当前滚动条的位置,利用slice() 去截取当前需要显示的内容  重点:滚动条的高度是有内层容器的paddingBottom 和 paddingTop 属性顶起来了,确保滚动条位置的准确性  这里鼠标上下滚动会出现闪屏问题:解决方案如下:      方案一:  预加载:                    向下预加载:                        比如div滚动区域显示30行,就预加载 300行( 即这里 slice(startIndex,startIndex + 300) ),                    向上预加载:                        在滚动监听事件函数中(computeRow)判断inner的paddingTop和paddingBottom即可                    当然这里的download-box的padding有30px像素,在加一个div,overflow:hidded就解决了      方案二:缩小滚动范围或者节流时间缩短,这里写的500ms

具体代码

  <template>    <div class="enn">      <div class="download-box txt" id="scrollable-div" @scroll="handleScroll">        <div id="inner">          <div v-for="(item, index) in data2" :key="index" class="line-box">            <div :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }">              {{ item }}            </div>          </div>        </div>      </div>    </div>  </template>  <script lang="ts" setup>  import { onMounted, PropType, ref } from 'vue';  import { useText } from './hooks/useText';  const props = defineProps({    baseData: {      type: Object as PropType<{        taskId: string;        barcodeName: string;      }>,      default: {},    },    collapsed: {      type: Boolean,      default: true,    },    type: {      type: Boolean,      default: false,    },  });  const { data } = useText(props.type);  //  这里大数据量数组是  data.geneTexts  /**   * 虚拟列表滚动大致思路:两个div容器   *   *    外层:外部容器用来固定列表容器的高度,同时生成滚动条   *   *    内层:内部容器用来装元素,高度是所有元素高度的和   *   *    外层容器鼠标滚动事件  dom.scrollTop 获取滚动条的位置   *   *    根据每行列表的高以及当前滚动条的位置,利用slice() 去截取当前需要显示的内容   *   *    重点:滚动条的高度是有内层容器的paddingBottom 和 paddingTop 属性顶起来了,确保滚动条位置的准确性   *   *    这里鼠标上下滚动会出现闪屏问题:解决方案如下:   *   *        方案一:  预加载:   *   *                      向下预加载:   *                          比如div滚动区域显示30行,就预加载 300行( 即这里 slice(startIndex,startIndex + 300) ),   *   *                      向上预加载:   *                          在滚动监听事件函数中(computeRow)判断inner的paddingTop和paddingBottom即可   *   *                      当然这里的download-box的padding有30px像素,在加一个div,overflow:hidded就解决了   *   *        方案二:缩小滚动范围或者节流时间缩短,这里写的500ms   *   *   */  let timer_throttle: any;  const throttle = (func: Function, wait?: number) => {    wait = wait || 500;    if (!timer_throttle) {      timer_throttle = setTimeout(() => {        func.apply(this);        timer_throttle = null;      }, wait);    }  };  // 鼠标滚动事件  const handleScroll = (event: any) => throttle(computeRow, 100);  // 计算当前显示tab  const computeRow = () => {    // console.log('距离顶部距离', window.scrollY, geneTexts);    let scrollableDiv = document.getElementById('scrollable-div');    let topPosition = scrollableDiv.scrollTop;    let leftPosition = scrollableDiv.scrollLeft;    console.log('垂直滚动位置:', topPosition, '水平滚动位置:', leftPosition);    const startIndex = Math.max(0, Math.floor(topPosition / 30));       const endIndex = startIndex + 300;    data2.value = data.geneTexts.slice(startIndex, endIndex);    let inner = document.getElementById('inner');    if (topPosition < 2700) {      // 向上预计加载,这里判断了三个高度,可以多判断几个,增加流畅度      inner.style.paddingTop = topPosition + 'px';      inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - topPosition + 'px';    } else if (topPosition + data2.value.length * 30 >= data.geneTexts.length * 30) {      // 这里 9000 是 内层div的高度 30 * 300   理解div高度是 padding+div内容高度      inner.style.paddingTop = topPosition - 900 + 'px'; //900 是div的高度      inner.style.paddingBottom = 0 + 'px';    } else {      inner.style.paddingTop = topPosition - 2700 + 'px';      inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 + 2700 - topPosition + 'px';    }  };  const data2 = ref([]);  const init = () => {    data2.value = data.geneTexts.slice(0, 300);    let inner = document.getElementById('inner');    inner.style.paddingTop = 0 + 'px';    inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - 900 + 'px';  };  </script>  <style lang="less" scoped>  .button-box {    margin-bottom: 25px;    .flex-type(flex-end);    :deep(.ant-btn) {      margin-left: 10px;    }  }  .enn {    background: #282c34;    outline: 1px solid red;    padding: 30px 20px;    height: 960px;  }  .download-box {    width: 100%;    // padding: 30px 20px;    outline: 1px solid rgb(17, 0, 255);    background-color: #fff;    overflow: hidden;    .line-box {      .flex-type(flex-start);      height: 30px;    }    &.txt {      background: #282c34;      color: #fff;      height: 900px;      overflow: auto;    }  }  </style>

替代方案

上面是自己写的,github上面还有好多插件可以用,但各有优劣,根据自己需求选择
如:

vue-virtual-scroller

https://github.com/Akryum/vue-virtual-scroller/tree/0f2e36248421ad69f41c9a08b8dcf7839527b8c2

vue-virt-list

vue-draggable-virtual-scroll-list

virtual-list

自己找吧,我就不一一列举了,看图

在这里插入图片描述

<template>  <br />  <div>    <Table      :columns="tableConfig.columns"      :data="tableConfig.totalData"      :loading="tableConfig.loading"      :pagination="false"    ></Table>  </div>  <br />  <div class="button-box">    <a-select      v-model:value="selection"      placeholder="请选择序列"      :options="seqOptions"      @change="        (selection:string) => handleChangeSeq(baseData.taskId, baseData.barcodeName, width, selection)      "    ></a-select>    <a-button type="primary" @click="handleClickExport()">导出所有序列</a-button>    <a-button type="primary" @click="modalConfig.visible = true">导出当前序列</a-button>  </div>  <!-- <SeqText :collapsed="props.collapsed" :seqText="data.geneTexts" /> -->  <div class="enn">    <div class="download-box txt" id="scrollable-div" @scroll="handleScroll">      <div id="inner">        <div v-for="(item, index) in data2" :key="index" class="line-box">          <template v-if="index === 0 && start === 0">            <div :class="{ 'text-title': props.collapsed, 'text-title-samll': !props.collapsed }">              {{ item }}            </div>          </template>          <template v-else>            <div :class="{ 'text-number': props.collapsed, 'text-number-samll': !props.collapsed }">              {{ calLine(item, index + start) }}            </div>            <div :class="{ 'text-box': props.collapsed, 'text-box-samll': !props.collapsed }">              {{ item }}            </div>            <div              :class="{ 'text-number2': props.collapsed, 'text-number2-samll': !props.collapsed }"            >              {{ endRow(item, index + start) }}            </div>          </template>        </div>      </div>    </div>  </div>  <br />  <a-modal    title="导出文件"    :visible="modalConfig.visible"    @ok="handleExport(data.geneTexts)"    @cancel="modalConfig.visible = false"  >    <div class="form-box">      <a-form>        <a-form-item label="自定义文件名">          <a-input v-model:value="modalConfig.name" placeholder="请输入自定义文件名"></a-input>        </a-form-item>      </a-form>    </div>  </a-modal></template><script lang="ts" setup>import { defineComponent, onMounted, PropType, ref } from 'vue';import Table from '/@/components/table/sTable.vue';import SeqText from '/@/components/SeqText/index.vue';import { useText, useTable } from './hooks/useText';import { useModal } from './hooks/useModal';import { serverAddress } from '/@/serve/index';import { download, downloadTxt } from '/@/libs/utils/download';const props = defineProps({  /**   * 基础数据   */  baseData: {    type: Object as PropType<{      taskId: string;      barcodeName: string;    }>,    default: {},  },  collapsed: {    type: Boolean,    default: true,  },  type: {    type: Boolean,    default: false,  },});let width = 100;const { taskId, barcodeName } = props.baseData;const { data, getMedaka, getAvailableSeq, handleChangeSeq, seqOptions, selection } = useText(  props.type,);const { tableConfig, getTable } = useTable(props.type);const VITE_APP_URL = serverAddress();const { modalConfig, handleExport } = useModal();const handleClickExport = () => {  let path = '';  if (props.type) {    path = VITE_APP_URL + `outputs/${taskId}/fastq_analysis/${barcodeName}/ragtag.fasta`;  } else {    path =      VITE_APP_URL + `outputs/${taskId}/fastq_analysis/${barcodeName}/${barcodeName}.final.fasta`;  }  download(path, '.fasta');};const calLine = (item: any, index: number) => {  return width * (index - 1) + 1;};const endRow = (item: any, index: number) => {  return width * index;};onMounted(() => {  getAvailableSeq(taskId, barcodeName).then(() => {    if (seqOptions.value.length > 0) {      getMedaka(taskId, barcodeName, width, seqOptions.value[0].value).then(() => init());      // getMedaka(taskId, barcodeName, width);    }  });  getTable(taskId, barcodeName);});/** * 虚拟列表滚动大致思路:两个div容器 * *    外层:外部容器用来固定列表容器的高度,同时生成滚动条 * *    内层:内部容器用来装元素,高度是所有元素高度的和 * *    外层容器鼠标滚动事件  dom.scrollTop 获取滚动条的位置 * *    根据每行列表的高以及当前滚动条的位置,利用slice() 去截取当前需要显示的内容 * *    重点:滚动条的高度是有内层容器的paddingBottom 和 paddingTop 属性顶起来了,确保滚动条位置的准确性 * *    这里鼠标上下滚动会出现闪屏问题:解决方案如下: * *        方案一:  预加载: *  *                      向下预加载: *                          比如div滚动区域显示30行,就预加载 300行( 即这里 slice(startIndex,startIndex + 300) ), *   *                      向上预加载: *                          在滚动监听事件函数中(computeRow)判断inner的paddingTop和paddingBottom即可 *   *                      当然这里的download-box的padding有30px像素,在加一个div,overflow:hidded就解决了 * *        方案二:缩小滚动范围或者节流时间缩短,这里写的500ms *  * */let timer_throttle: any;const throttle = (func: Function, wait?: number) => {  wait = wait || 500;  if (!timer_throttle) {    timer_throttle = setTimeout(() => {      func.apply(this);      timer_throttle = null;    }, wait);  }};let start = ref(0);// 鼠标滚动事件const handleScroll = (event: any) => throttle(computeRow, 100);// 计算当前显示tabconst computeRow = () => {  // console.log('距离顶部距离', window.scrollY, geneTexts);  let scrollableDiv = document.getElementById('scrollable-div');  let topPosition = scrollableDiv.scrollTop;  let leftPosition = scrollableDiv.scrollLeft;  console.log('垂直滚动位置:', topPosition, '水平滚动位置:', leftPosition);  const startIndex = Math.max(0, Math.floor(topPosition / 30));  start.value = startIndex;  const endIndex = startIndex + 300;  data2.value = data.geneTexts.slice(startIndex, endIndex);  let inner = document.getElementById('inner');  if (topPosition < 2700) {    // 向上预计加载,这里判断了三个高度,可以多判断几个,增加流畅度    inner.style.paddingTop = topPosition + 'px';    inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - topPosition + 'px';  } else if (topPosition + data2.value.length * 30 >= data.geneTexts.length * 30) {    // 这里 9000 是 内层div的高度 30 * 300    inner.style.paddingTop = topPosition - 900 + 'px'; //900 是div的高度    inner.style.paddingBottom = 0 + 'px';  } else {    inner.style.paddingTop = topPosition - 2700 + 'px';    inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 + 2700 - topPosition + 'px';  }};const data2 = ref([]);const init = () => {  data2.value = data.geneTexts.slice(0, 300);  let inner = document.getElementById('inner');  inner.style.paddingTop = 0 + 'px';  inner.style.paddingBottom = (data.geneTexts.length + 2) * 30 - 900 + 'px';};</script><style lang="less" scoped>// @import '../../../../assets/styles/views/medaka.less';.button-box {  margin-bottom: 25px;  .flex-type(flex-end);  :deep(.ant-btn) {    margin-left: 10px;  }}.enn {  background: #282c34;  outline: 1px solid red;  padding: 30px 20px;  height: 960px;}.download-box {  width: 100%;  // padding: 30px 20px;  outline: 1px solid rgb(17, 0, 255);  background-color: #fff;  overflow: hidden;  .line-box {    .flex-type(flex-start);    height: 30px;  }  &.txt {    background: #282c34;    color: #fff;    height: 900px;    overflow: auto;    .el-row {      display: flex;      align-items: center;      margin-bottom: 10px;      margin: auto;      font-size: 22px;    }  }}.form-box {  .flex-type(center);}:deep(.ant-select-selector) {  min-width: 120px;}@media screen and (min-width: 1842px) {  .text-box-samll {    letter-spacing: 1.5px;    font-size: 15px;  }  .text-number-samll {    min-width: 60px;    font-size: 15px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 60px;    font-size: 15px;  }  .text-title-samll {    font-size: 15px;  }  .text-box {    font-size: 22px;    // letter-spacing: 3px;  }  .text-number {    min-width: 100px;    font-size: 22px;  }  .text-number2 {    margin-left: 20px;    min-width: 100px;    font-size: 22px;  }  .text-title {    font-size: 22px;  }}@media screen and (min-width: 1600px) and (max-width: 1841px) {  .text-box-samll {    font-size: 15px;  }  .text-number-samll {    min-width: 40px;    font-size: 15px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 15px;  }  .text-title-samll {    font-size: 15px;  }  .text-box {    font-size: 20px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 15px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 20px;  }  .text-title {    font-size: 20px;  }}@media screen and (min-width: 1443px) and (max-width: 1599px) {  .text-box-samll {    font-size: 13px;  }  .text-number-samll {    min-width: 40px;    font-size: 13px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 13px;  }  .text-title-samll {    font-size: 13px;  }  .text-box {    font-size: 18px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 15px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 18px;  }  .text-title {    font-size: 18px;  }}@media screen and (max-width: 1442px) {  .text-box-samll {    font-size: 11px;  }  .text-number-samll {    min-width: 40px;    font-size: 11px;  }  .text-number2-samll {    margin-left: 20px;    min-width: 40px;    font-size: 11px;  }  .text-title-samll {    font-size: 11px;  }  .text-box {    font-size: 16px;    // letter-spacing: 1.2px;  }  .text-number {    min-width: 60px;    font-size: 15px;  }  .text-number2 {    margin-left: 20px;    min-width: 60px;    font-size: 16px;  }  .text-title {    font-size: 16px;  }}</style>

点击全文阅读


本文链接:http://m.zhangshiyu.com/post/186562.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1