【项目数据优化三】长列表数据优化

简介: 【项目数据优化三】长列表数据优化

数字化管理平台

Vue3+Vite+VueRouter+Pinia+Axios+ElementPlus

Vue权限系统案例

个人博客地址

前言:

在移动端项目中,我们通常会使用懒加载来加载列表,优点很明显,不需要一次将数据请求完,当用户下拉到底部时,才使用ajax动态从服务器拉取接下来的数据。但是这又导致了一个问题,如果用户疯狂进行下拉呢,这就会导致浏览器创建多个多余的节点,出现冗余,并且你拥有多少个节点,vue就会diff多少个节点,这样的场景会带来多余的性能消耗和内存占用。试想一下,如果我们能只渲染用户可视区域的节点,便能很好的解决这个问题,这便是虚拟滚动的产生背景。

虚拟滚动原理:

首先我们要知道,虚拟滚动是用到 Vue 的 v-for 实现的,上面也解释了,虚拟滚动是只渲染可视区域,那么我们可视区域的节点内容必然会随着用户的滚动条的改变而改变,假设一个页面就只能显示 n 个节点,那么我们如何让这n个需要变动的节点跟着滚动条动呢?

使用CSStransform:translateY(),这样,我们只需要让这n个节点跟着滚动条移动,我们滚动到哪里,节点替换到哪里。

要实现虚拟滚动你只需要知道以下条件:

  1. 1. 一个页面能显示多少个item?

页面容量 = 页面大小(clientHeight)/单个item大小

showNum = Math.floor(viewH / itemH) + 4 # 这里多设置几个防止滚动时候直接替换,所以 +4

  1. 2. 应该从哪个节点开始渲染?

我们假设现在滚动条滚动到了x的位置,我们是否就可以计算出这个x这个高度可以容纳多少个节点,进而得出应该从哪个节点开始渲染呢?答案是肯定的,js为我们提供了scrollTop这个属性来获取滚动条卷入的高度。

 getCurStart(scrollTop){
  // 卷去了多少个
  return Math.floor(scrollTop/(itemHeight));
}
  1. 3. 什么时候渲染?

渲染时机也很简单,列表中第一个节点被完全卷入的时候我们需要执行渲染,因为这个时候被完全卷去的节点已经完全看不到了,我们需要将它顶下来然后再将其渲染成下一项的数据,这么说可能有点难理解,放张图吧~

800c5f7edc4b4b61b29af55fae030ca7.png

如上图,当1被卷去的时候(完全离开我们的可视区),我们就利用css的translateY将它顶下来,渲染成2,你会发现发现在可视区域外多出来了一个节点,为了保证滑动的连贯性,你可以多设置几个冗余节点。

  1. 4. 如何渲染

这部分就是最核心的代码了。在这会出现一个问题,由于js并不是对每次都会对高频触发的回调进行响应,你如果不获取一个可以被itemHeight整除的偏移量,你极有可能拉回去的时候看到第一个节点的偏移量并不是0。

onScroll(){
  //scrollTop常量记录当前滚动的高度
  const scrollTop=this.$refs.list.scrollTop;
  const start=this.getCurStart(scrollTop);
  //对比上一次的开始节点 比较是否发生变化,发生变化后便重新渲染列表
  if(this.start!=start){
    //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量
    const offsetY = scrollTop - (scrollTop % this.itemHeight);
    //使用slice拿到需要渲染的那一部分
    this.renderList=this.list.slice(start,this.start+this.showNum);
    //这里的top用于设置translateY  transform:translateY(${top}px)
    this.top=offsetY;
  }
  this.start=start;
}
  1. 5. 优化处理

onScroll属高频触发回调,为了节约性能消耗,我们需对其加以限制,让其最短50ms触发一次。以下是封装节流函数。

# throttle.js 
export default function(fn, delay) {
    let lock = false;
    return (...args) => {
        if (lock)
            return;
        //进入加锁
        lock = true;
        setTimeout(() => {
            fn.apply(this, args);
            //执行完毕解锁
            lock = false;
        }, delay);
    }
}

页面中滚动完整代码(Vue2 Options API):

<template>
    <div class="list" @scroll="scrollHandle" ref="list">
        <div class="item" v-for="(item,index) in renderList" :key="index"  :style="`height:${itemHeight}px;line-height:${itemHeight}px;transform:translateY(${top}px)`">
          {{item}}
        </div>
    </div>
</template>
<script>
import throttle from '@/utils/throttle';
export default {
  name: 'App',
  data() {
    return {
      list:[],//完整列表
      itemHeight:60,//每一项的高度
      renderList:[],//需要渲染的列表
      start:0,//开始渲染的位置
      showNum:0,//页面的容积:能装下多少个节点
      top:0,
      scroll,//用于初始化节流
    }
  },
  mounted() {
    this.initList();
    const cHeight=document.documentElement.clientHeight
    //计算页面能容纳下几个节点并且设置四个节点作为冗余
    this.showNum=Math.floor(cHeight/this.itemHeight)+4;
    //设置要渲染的列表 设置成能够容纳下的最大元素个数
    this.renderList=this.list.slice(0,this.showNum);
    //初始化节流函数 最短50毫秒触发一次
    this.scroll=throttle(this.onScroll,50);
  },
  methods: {
    //初始化列表 ,循环渲染 500条
    initList(){
      for(let i=0;i<500;i++){
        this.list.push(i);
      }
    },
    scrollHandle(){
      this.scroll();
    },
    onScroll(){
      //scrollTop常量记录当前滚动的高度
      const scrollTop=this.$refs.list.scrollTop;
      const start=this.getCurStart(scrollTop);
      //对比上一次的开始节点 比较是否发生变化,发生变化后便重新渲染列表
      if(this.start!=start){
        //在这需要获得一个可以被itemHeight整除的数来作为item的偏移量
        const offsetY = scrollTop - (scrollTop % this.itemHeight);
        //使用slice拿到需要渲染的那一部分
        this.renderList=this.list.slice(start,this.start+this.showNum);
        //这里的top用于设置translateY  transform:translateY(${top}px)
        this.top=offsetY;
      }
      this.start=start;
    },
    getCurStart(scrollTop){
      //卷去了多少个
      return Math.floor(scrollTop/(this.itemHeight));
    }
  },
}
</script>
<style>
    *{
      margin: 0;
      padding: 0;
    }
    .list{
      height: 100vh;
      overflow: scroll;
    }
    .item{
      text-align: center;
      width: 100%;
      box-sizing: border-box;
      border-bottom: 1px solid lightgray;
    }
</style>

容器中滚动代码(Vue3 Composition API)

<script setup>
    /**
     * 项目列表数据越来越多(上万条),正常列表可以分页,但是像下拉框之类组件就不能分页。每次都要加载所有的(很慢),性能不好的浏览器特别卡顿。虚拟滚动的技术完美解决。
     * 主要用于无法使用分页功能的长列表首屏加载速度慢问题,DOM加载过多“无用”元素。
     * 核心:
     *      1. 元素监听scroll事件
     *      2. 计算可视化高度一次能装几个列表,然后从总数据中进行slice截取
     *      3. 每一次滚动后根据scrollTop值获取一个可以整除itemH结果进行偏移
     */
    import { onMounted, reactive, ref } from 'vue';
    const listEle = ref()
    // 上万条总数居
    // const list = reactive(Array.apply(null, { length: 100000 }).map((v, i) => i))
    const list = reactive(Array.from({ length: 100000 }).map((_, i) => {
        return {
            key: i,
            value: i + 1
        }
    }))
    // 页面高度
    const viewH = 800
    // 单项高度
    let itemH = 200
    // 整个滚动列表高度
    let scrollH = itemH * list.length
    // 可视化高度一次可装列表数量(多设置几个防止滚动时候直接替换)
    let showNum = Math.floor(viewH / itemH) + 4
    // 页面上展示的数据
    let showList = reactive(list.slice(0, showNum))
    // 动态偏移量
    let offsetY = ref(0)
    // 时间戳
    let latestTime = new Date().getTime()
    onMounted(() => {
    })
    let timer = ref(null)
    const handleScroll = (e) => {
        if (new Date().getTime() - latestTime > 10) {
            clearInterval(timer.value)
            timer.value = setTimeout(() => {
                // 获取卷去的高度
                let scrollTop = e.target.scrollTop
                // 每一次滚动后,根据卷去高度 scrollTop 值,获取一个可以整除单项高度 itemH 的结果进行偏移
                // 例如: 卷去的 scrllTop = 1020  1020 % itemH = 20  offsetY = 1000
                offsetY.value = scrollTop - (scrollTop % itemH)
                // 更新数据:被卷去几条数据,就要往下增加几条数据
                showList = list.slice(Math.floor(offsetY.value / itemH), Math.floor(offsetY.value / itemH) + showNum)
                // 更新时间戳
                latestTime = new Date().getTime()
                console.log(showList)
            }, 300);
        }
    }
</script>
<template>
    <div :style="`height:${viewH}px;overflow-y:scroll;`" @scroll="handleScroll">
        <ul ref="listEle">
            <li v-for="item in showList" :key="item.key"
                :style="{ 'transform': `translateY(${offsetY}px)`, 'height': `${itemH}px`, 'line-height': `${itemH}px` }">{{
                    item.value }}</li>
        </ul>
    </div>
</template>
<style scoped>
    li {
        /* height: 200px;
        line-height: 200px; */
        color: #fff;
        font-size: 50px;
        font-weight: bold;
        text-align: center;
        background-color: orange;
    }
    li:nth-of-type(odd) {
        background-color: blue;
    }
</style>


相关文章
|
7月前
|
缓存 关系型数据库 MySQL
MySQL查询优化:提速查询效率的13大秘籍(合理使用索引合并、优化配置参数、使用分区优化性能、避免不必要的排序和group by操作)(下)
MySQL查询优化:提速查询效率的13大秘籍(合理使用索引合并、优化配置参数、使用分区优化性能、避免不必要的排序和group by操作)(下)
302 0
|
7月前
|
缓存 关系型数据库 MySQL
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
MySQL 查询优化:提速查询效率的13大秘籍(索引设计、查询优化、缓存策略、子查询优化以及定期表分析和优化)(中)
1124 0
|
5月前
|
运维 关系型数据库 分布式数据库
PolarDB产品使用问题之将部分表设置为压缩表,是否会对节点的整体性能影响
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
SQL 关系型数据库 MySQL
mysql聚合统计数据查询缓慢优化方案
在我们日常操作数据库的时候,比如订单表、访问记录表、商品表的时候。 经常会处理计算数据列总和、数据行数等统计问题。 随着业务发展,这些表会越来越大,如果处理不当,查询统计的速度也会越来越慢,直到业务无法再容忍。 所以,我们需要先了解、思考这些场景知识点,在设计之初,便预留一些优化空间支撑业务发展。
752 0
|
SQL 关系型数据库 MySQL
索引单表优化案例|学习笔记
快速学习索引单表优化案例
124 0
|
SQL 关系型数据库 MySQL
mysql优化大批量数据时的分页操作
mysql大数据量下的优化分页的方式,仅供参考。
mysql优化大批量数据时的分页操作
|
SQL 关系型数据库 MySQL
mysql性能优化:单表1400w查询最后十条数据(耗时0.036s)
看几个关键字段,type、key、extra,不算完美,但也还行,毕竟我们这种非DBA选手,sql能力有限 顺便科普下这个执行计划,看id列,1 1 2,执行顺序是第三行 第一行 第二行,记住口诀:id不同大的先走,id相同,从上往下
1450 0
mysql性能优化:单表1400w查询最后十条数据(耗时0.036s)
|
Web App开发 关系型数据库 测试技术
PostgreSQL pageinspect 诊断与优化GIN (倒排) 索引合并延迟导致的查询性能下降问题
标签 PostgreSQL , brin索引 , gin索引 , 合并延迟 , gin_pending_list_limit , 查询性能下降 背景 GIN索引为PostgreSQL数据库多值类型的倒排索引,一条记录可能涉及到多个GIN索引中的KEY,所以如果写入时实时合并索引,会导致IO急剧增加,写入RT必然增加。
1906 0
|
算法 Java 数据库连接
大主子表关联的性能优化方法
一、        原理解释所谓主子表关联计算,就是针对主表的每条记录,按关联字段找到子表中对应的一批记录。以订单(主表)和订单明细(子表)为例,两者以订单ID为关联字段。下图显示了关联计算过程中对主表中一条记录的处理情况,红色箭头代表没找到对应记录(不可关联),绿色箭头代表找到了对应记录(可关联):                                               假设订单(主表)有m条记录,订单明细(子表)有n条记录,在不考虑优化算法时,主表中每一条记录的关联都需要遍历子表,相应的时间复杂度为O(n)。
1436 0